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
virtualdeclara uma implementação de base que pode ser substituída.abstractdeclara um membro sem implementação na classe abstrata e obriga classes concretas a implementá-lo.overridesubstitui um membro virtual ou abstrato herdado; o compilador verifica que existe algo válido para sobrescrever.sealed overridesobrescreve 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.