Como funciona a herança em C#

Uma classe derivada declara a classe base depois de dois-pontos. Ela recebe os membros acessíveis da base e pode adicionar membros, substituir pontos de extensão virtuais ou implementar contratos. C# permite herdar de uma única classe base e implementar várias interfaces. Uma classe sem base explícita deriva de object.

public abstract class Forma
{
    public abstract decimal CalcularArea();
}

public sealed class Retangulo(decimal largura, decimal altura) : Forma
{
    public override decimal CalcularArea() => largura * altura;
}

A base define o que é comum e o que pode variar. abstract impede instanciar diretamente a base e pode declarar operações que derivados devem implementar. virtual fornece comportamento padrão substituível. override fornece a implementação especializada. Uma classe sealed não permite derivação.

Teste a relação de substituição

Se o código que aceita uma instância da base funciona corretamente com qualquer derivada, a hierarquia oferece substituição útil. Isso exige preservar pré-condições, pós-condições e invariantes do contrato. Um subtipo não deve aceitar menos casos válidos, prometer menos resultados ou deixar o estado inválido que a base prometeu evitar.

O exemplo de formas evita assumir que todo tipo derivado tenha um comportamento incompatível com a operação comum. Uma hierarquia suspeita seria chamar Quadrado de Retangulo mutável, mas redefinir um lado alterar silenciosamente o outro quando consumidores esperam dimensões independentes. O nome da relação pode ser verdadeiro informalmente e ainda quebrar o contrato do código.

Esse cuidado é uma aplicação do Liskov Substitution Principle. Se consumidores precisam verificar o tipo concreto e ramificar para corrigir o comportamento, reavalie a abstração ou separe os contratos.

Uma classe ou uma interface?

Use uma classe base quando há uma relação estável de especialização e compartilhar implementação ou estado protegido é parte intencional do contrato. Use uma interface quando quer expressar uma capacidade que tipos de linhagens diferentes podem oferecer, como ISerializavel ou IPagavel. Interfaces não compartilham estado de instância como uma classe base, mas podem oferecer implementações padrão de membros em versões modernas de C#.

Uma interface não é automaticamente melhor: muitos contratos pequenos sem consumidores reais também criam fragmentação. Em tipos de domínio, considere se a abstração descreve uma capacidade ou se está apenas duplicando a classe concreta com um prefixo I.

Quando composição é mais flexível

Composição coloca um objeto dentro de outro e delega parte do trabalho a ele. Ela representa “tem um” em vez de “é um”, permite combinar comportamentos e trocar a implementação sem herdar detalhes internos. É especialmente útil quando as capacidades variam de forma independente ou a relação muda durante a vida do objeto.

public interface ICalculadoraFrete
{
    decimal Calcular(decimal peso, decimal distancia);
}

public sealed class Pedido(ICalculadoraFrete calculadora)
{
    public decimal CalcularFrete(decimal peso, decimal distancia) =>
        calculadora.Calcular(peso, distancia);
}

O pedido usa um colaborador sem precisar herdar de uma implementação de frete. Esse desenho facilita trocar a política e testar o contrato. Para uma única operação estável e sem variantes, porém, introduzir abstrações e delegação pode ser custo sem benefício.

Custos e cuidados de hierarquias

  • Uma mudança na base pode alterar o comportamento de derivados que não controlamos diretamente.
  • Membros protected ampliam o contrato para subclasses e dificultam refatorações internas.
  • Hierarquias profundas tornam o comportamento efetivo difícil de localizar.
  • Classes-base reutilizadas por conveniência podem impor estado e dependências irrelevantes ao subtipo.
  • Construtores de base devem evitar chamar métodos virtuais antes de o objeto derivado estar inicializado.

Prefira manter hierarquias pequenas e contratos explícitos. Uma classe pode ser selada até existir um caso concreto de extensão; abrir um ponto virtual é uma decisão de API, não apenas uma palavra-chave.

Herança e records, structs e métodos

Classes podem herdar de uma classe definida pelo usuário. Structs não herdam dessa forma, embora implementem interfaces. Records de classe podem herdar apenas de outros records de classe e seguem regras específicas de igualdade; record structs não têm herança de classe. Veja a comparação entre class, struct e record.

Para substituir comportamento, o membro da base precisa ser virtual ou abstract; o membro derivado usa override. Para aprofundar o despacho em tempo de execução e a diferença para new, consulte polimorfismo e override.