Menu

Herança em C#: base, virtual, override, sealed e polimorfismo

Como a herança de classes funciona em C#: derivar com dois pontos, o que é e o que não é herdado, chamar a classe base com base, virtual e override, ocultar com new (e a saída que surpreende todo mundo), sealed, polimorfismo e casts para cima e para baixo na hierarquia.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

A herança permite que uma classe seja construída sobre outra. A classe derivada recebe os membros da classe base, adiciona os seus e pode substituir o comportamento que a classe base permite substituir. Combinada com métodos virtual, ela dá o polimorfismo: código escrito para a classe base executa o comportamento derivado certo sem saber qual classe derivada tem em mãos.

Derivando uma classe

Coloque a classe base depois de dois pontos. A classe derivada tem tudo o que a classe base tem, mais o que ela declara:

Saída:

TX-19: 20 km, 2 fares
True

O que uma classe derivada recebe e o que não recebe:

  • Herdado: campos, propriedades, métodos, eventos e tipos aninhados. Tudo está fisicamente no objeto.
  • Acessível: só o que a base permite: membros public, protected e internal. Um membro private de Vehicle existe dentro de todo Taxi, mas o código de Taxi não consegue nomeá-lo. É por isso que Km tem um private set: Taxi pode lê-lo e só o muda por meio de Drive.
  • Não herdado: construtores. Taxi precisa declarar os seus e encadear um dos de Vehicle com : base(plate). A página de construtores mostra a ordem em que os dois executam.

Toda classe deriva, em última instância, de object, e é por isso que todo objeto tem ToString(), Equals() e GetHashCode().

virtual e override

Uma classe base marca um método como virtual para dizer "as classes derivadas podem fornecer a sua versão". Uma classe derivada o substitui com override. Dentro da sobrescrita, base.Method() chama a versão da classe base.

Saída:

[email] to lea@example.com: Your order 1042 has shipped today
  (unsubscribe link appended)
[sms] to +351 912 000 111: Your order 1042 has...
[generic] to ops-team: Your order 1042 has shipped today

A variável do laço é do tipo Notification, e mesmo assim cada objeto se renderiza do seu jeito. Isso é polimorfismo: a chamada n.Render(...) é resolvida em tempo de execução a partir do tipo real do objeto. Repare também que base.Render em Email usa Channel, que também é virtual, então o método da base imprime email, não generic. Uma chamada virtual dentro da classe base ainda chega à sobrescrita.

Propriedades também podem ser virtuais, como Channel mostra. Campos não.

O compilador cobra as duas palavras-chave. Escrever override em um método que não é virtual é o erro CS0506 ("cannot override inherited member ... because it is not marked virtual, abstract, or override"). Esquecer o override quando o método da base é virtual é só um aviso, e muda completamente o significado, como mostra a próxima seção.

new vs override: ocultar em vez de sobrescrever

Se uma classe derivada declara um método com a mesma assinatura de um método da base sem escrever override, ela oculta o método da base. O compilador avisa (CS0114 para um método virtual da base, CS0108 nos outros casos) e sugere a palavra-chave new, que silencia o aviso mas mantém o comportamento de ocultar:

Saída:

Sales report
Report
Draft report

b e c são o mesmo tipo de objeto, e mesmo assim imprimem títulos diferentes. Com new, o método escolhido depende do tipo da variável, decidido em tempo de compilação. Código que trata relatórios como Report (uma lista, um parâmetro de método, um callback de framework) nunca vê DraftReport.Title. Isso quase nunca é o que você quer. Use override para polimorfismo; new existe principalmente para o caso em que uma classe base que você não controla adiciona um membro cujo nome colide com um que você já tem.

sealed

sealed em uma classe proíbe derivar dela:

sealed class Invoice { }
class CorrectedInvoice : Invoice { }
// error CS0509: 'CorrectedInvoice': cannot derive from sealed type 'Invoice'

string é sealed, assim como muitos tipos do framework. Em uma sobrescrita, sealed para a cadeia naquele nível:

class Shape { public virtual string Name() => "shape"; }
class Square : Shape { public sealed override string Name() => "square"; }
class Tile : Square { public override string Name() => "tile"; }
// error CS0239: 'Tile.Name()': cannot override inherited member 'Square.Name()' because it is sealed

Projetar uma classe para herança dá trabalho: decidir o que é virtual, em que as classes derivadas podem confiar, em que ordem as coisas acontecem. Uma classe que não foi projetada assim fica mais segura selada, e selar pode ser desfeito depois sem quebrar ninguém, enquanto tirar o sealed não tem volta quando outros já derivam de você. Chamadas a membros de classes seladas também podem ser um pouco mais rápidas, porque o runtime sabe que não existe sobrescrita.

Uma classe base, muitas interfaces

Uma classe em C# tem exatamente uma classe base. class Admin : User, Employee é o erro CS1721 ("cannot have multiple base classes"). Uma classe pode, porém, implementar qualquer número de interfaces junto com a classe base:

class Admin : User, IAuditable, IComparable<Admin>
{
    // base class first, then interfaces, in any order
}

Use uma classe base para "é um tipo de, e compartilha implementação com", e interfaces para "sabe fazer". Quando a classe base só existe para obrigar as classes derivadas a preencher alguns métodos, a ferramenta é uma classe abstrata.

Casts para cima e para baixo na hierarquia

Um objeto derivado sempre pode ser usado onde o tipo base é esperado. Esse upcast é implícito e não pode falhar. No sentido contrário, um downcast precisa de um cast explícito e falha em tempo de execução se o objeto não for daquele tipo:

Saída:

Rex fetches the ball
InvalidCastException: Tom is not a Dog
True
Rex fetches the ball

Escrever Dog d = pet; sem o cast é um erro de compilação (CS0266: "An explicit conversion exists (are you missing a cast?)"), porque o compilador só sabe que pet é algum Animal. Prefira is com uma variável quando o objeto pode ser de outro tipo, e um cast simples só quando qualquer outra coisa seria um bug. Downcasts frequentes são um sinal de problema de design: normalmente significam que o comportamento deveria estar em um método virtual da classe base. A página de pattern matching cobre as formas de is por completo.

Herança e coleções

O polimorfismo funciona elemento por elemento, mas coleções genéricas de um tipo derivado não são coleções do tipo base: List<Animal> animals = new List<Dog>(); não compila, porque a lista passaria a aceitar um Cat. Visões somente leitura são covariantes, então IEnumerable<Animal> animals = new List<Dog>(); funciona. A página de genéricos explica por quê.

Erros comuns

  • Esquecer o override. O método compila com um aviso e oculta em silêncio em vez de sobrescrever. Trate o CS0114 como erro.
  • Tornar tudo virtual. Cada membro virtual é uma promessa às classes derivadas sobre quando ele é chamado e o que ele pode supor. Marque só os pontos de extensão que você pretende oferecer.
  • Hierarquias profundas. Três ou quatro níveis de herança tornam difícil saber qual versão de um método executa. Composição, uma classe que guarda outro objeto e o chama, costuma ser mais simples que um nível novo.
  • Chamar métodos virtuais a partir de um construtor. A sobrescrita executa antes do corpo do construtor derivado, então qualquer campo que esse corpo atribui ainda tem o valor padrão.
  • Usar herança só para reaproveitar código. Se uma Stack herda de List, quem chama pode fazer Insert no meio da sua pilha. Guarde uma List em um campo privado.

Perguntas frequentes

Como a herança funciona em C#?

Uma classe indica uma classe base depois de dois pontos: class Dog : Animal. A classe derivada recebe todos os membros da classe base (campos, propriedades, métodos, eventos), pode adicionar os seus e pode sobrescrever os que a base marcou como virtual. Construtores não são herdados, e os membros private, embora existam no objeto, não são acessíveis a partir da classe derivada.

Qual a diferença entre virtual e override em C#?

virtual vai no método da classe base e diz "as classes derivadas podem substituir isto". override vai no método da classe derivada e faz a substituição. Os dois são necessários: sobrescrever um método que não é virtual, abstract nem já override é o erro CS0506. Quando um método virtual é chamado, o runtime executa a versão do tipo real do objeto, não a do tipo da variável.

Qual a diferença entre new e override em C#?

override substitui o método da base para todo chamador, até para código que segura o objeto por meio de uma variável do tipo da classe base. new só o oculta: o código que vê o objeto como o tipo derivado chama o método novo, enquanto o código que o vê como o tipo base ainda chama o método da base. Então Base b = new Derived(); b.M(); executa Derived.M com override e Base.M com new.

C# suporta herança múltipla?

Não para classes: uma classe tem exatamente uma classe base, e listar duas é o erro CS1721. Uma classe pode implementar qualquer número de interfaces, que é como C# modela "este tipo sabe fazer várias coisas". Desde o C# 8, as interfaces também podem trazer implementações padrão de métodos.

O que sealed significa em C#?

Uma classe sealed não pode ser usada como classe base; derivar dela é o erro CS0509. string é sealed, por exemplo. Em um método, sealed override impede que as classes mais abaixo na hierarquia o sobrescrevam de novo (CS0239). Selar classes que não foram projetadas para herança é um padrão razoável.

Como chamar o método da classe base em C#?

Use base.MethodName(...) dentro da classe derivada, normalmente dentro da sobrescrita: public override string Describe() => base.Describe() + " with GPS";. Para construtores, use : base(...) depois da lista de parâmetros para escolher qual construtor da base executa.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR