Polimorfismo em tempo de execução

Uma referência de tipo base pode apontar para uma instância derivada. Se uma operação é virtual e foi sobrescrita, a implementação correspondente ao tipo real do objeto é executada. O consumidor depende do contrato, não precisa testar cada classe concreta.

public abstract class Notificacao
{
    public abstract string Formatar();
}

public sealed class NotificacaoEmail(string destino) : Notificacao
{
    public override string Formatar() => $"E-mail para {destino}";
}

public sealed class NotificacaoSms(string numero) : Notificacao
{
    public override string Formatar() => $"SMS para {numero}";
}

Notificacao mensagem = new NotificacaoEmail("equipe@example.com");
Console.WriteLine(mensagem.Formatar());

A variável tem tipo estático Notificacao; o objeto é uma NotificacaoEmail. Como Formatar é abstrato e sobrescrito, a chamada usa a implementação da instância concreta.

virtual, abstract e override

  • virtual declara uma implementação de base que pode ser substituída.
  • abstract declara um membro sem implementação na classe abstrata e obriga classes concretas a implementá-lo.
  • override substitui um membro virtual ou abstrato herdado; o compilador verifica que existe algo válido para sobrescrever.
  • sealed override sobrescreve o membro e impede novas subclasses de substituí-lo novamente.

Use override quando quer especializar comportamento preservando o mesmo contrato. Uma classe derivada também pode chamar base.Metodo() quando a implementação original faz parte deliberada do fluxo, mas isso não é obrigatório e pode ser inadequado se a base não for composta por etapas extensíveis.

Polimorfismo por interface

Uma variável cujo tipo é uma interface também pode referenciar várias implementações. Classes e structs podem implementar interfaces sem pertencer à mesma hierarquia de classes. Essa forma é útil quando o contrato descreve uma capacidade compartilhada, não uma relação de especialização.

public interface IExportador
{
    string Exportar();
}

public sealed class ExportadorCsv : IExportador
{
    public string Exportar() => "arquivo.csv";
}

public sealed class ExportadorJson : IExportador
{
    public string Exportar() => "arquivo.json";
}

O chamador pode receber IExportador e delegar a decisão ao objeto fornecido. O uso de interface não significa que toda dependência precise ser abstraída; o contrato deve representar uma fronteira ou capacidade que o sistema realmente usa.

Override não é sobrecarga

Sobrecarga (overload) declara métodos com o mesmo nome e assinaturas diferentes. O compilador escolhe qual método chamar a partir dos argumentos e tipos estáticos disponíveis. Não exige herança e não substitui dinamicamente uma implementação.

public decimal Calcular(decimal valor) => valor;
public decimal Calcular(decimal valor, decimal taxa) => valor + taxa;

Substituição (override) mantém a assinatura do membro virtual ou abstrato herdado, e a implementação é escolhida conforme o tipo real do objeto. O override é explícito justamente para indicar essa relação e permitir validação em tempo de compilação.

Override não é ocultação com new

O modificador new oculta um membro herdado com outro membro de mesmo nome; não o substitui no despacho virtual. Quando acessado por uma referência da classe base, a implementação da base pode continuar sendo chamada. Essa diferença costuma surpreender porque o resultado depende do tipo estático da referência.

class Base
{
    public virtual string Nome() => "Base";
}

class Derivada : Base
{
    public override string Nome() => "Derivada";
}

Uma chamada virtual via referência Base para uma instância Derivada resulta em “Derivada”. Se trocar override por new, a chamada por referência de tipo Base usa o membro da base. Em código de produção, não use new como atalho para contornar um contrato mal desenhado.

Quando o polimorfismo ajuda

Ele é útil quando várias variantes reais precisam responder à mesma operação e o consumidor não deve concentrar condicionais sobre os tipos concretos. Pode reduzir ramificações espalhadas, mas também introduz indireção e exige contratos consistentes. Para poucas alternativas estáveis, um switch explícito pode ser mais fácil de ler do que uma hierarquia prematura.

Quando há substituição por herança, toda implementação deve honrar o comportamento anunciado pela base. Veja também herança em C# e o princípio LSP em SOLID na prática.