C# virtual e override

Todo animal faz um som, mas cada um faz o seu: o cachorro late, o gato mia. Com herança, o FazerSom() pode morar na classe Animal, mas aí todos fariam o mesmo som. A solução do C# são duas palavras que trabalham juntas: virtual ("virtual"), na base, diz "este método pode ser trocado"; override ("sobrescrever"), na filha, diz "estou trocando".

Você vai aprender
  • Por que só escrever um método com o mesmo nome na filha não basta (o aviso CS0108).
  • virtual na base e override na filha.
  • Que, com override, o método certo roda até numa variável do tipo da base.
  • O aviso CS0114 e a palavra new: esconder × sobrescrever.
  • Sobrescrever ToString() e propriedades, e reaproveitar a base com base.Metodo().
  • sealed override e os erros CS0506, CS0115, CS0508, CS0507, CS0621 e CS0239.

A ideia: a receita da família

A avó da Ana tem uma receita de bolo. Os filhos herdaram a receita, mas ela avisou: "esta aqui vocês podem adaptar". A tia trocou o açúcar por mel; o pai da Ana seguiu igualzinho. Quando alguém pede "o bolo da família" na casa da tia, sai o bolo com mel.

  • "Pode adaptar" é o virtual: a base permite que as filhas tenham a sua versão.
  • "Adaptei" é o override: a filha escreve a sua versão, que substitui a da base.
  • Quem não adaptou (o pai da Ana) usa a receita original.

O problema: o método da filha que não é chamado

A primeira tentativa da Ana: um FazerSom em Animal e outro, com o mesmo nome, em Cachorro.

var rex = new Cachorro { Nome = "Rex" };
rex.FazerSom();

Animal bicho = rex;
bicho.FazerSom();

class Animal
{
    public string Nome { get; set; } = "";

    public void FazerSom()
    {
        Console.WriteLine($"{Nome} faz um som qualquer.");
    }
}

class Cachorro : Animal
{
    public void FazerSom()
    {
        Console.WriteLine($"{Nome}: Au au!");
    }
}
$ dotnet run sem-virtual.cs
/home/ana/cs/sem-virtual.cs(19,17): warning CS0108: "Cachorro.FazerSom()" oculta o membro herdado "Animal.FazerSom()". Use a nova palavra-chave se foi pretendido ocultar.
Rex: Au au!
Rex faz um som qualquer.          « o mesmo Rex, outro som!

Duas coisas estranhas:

  • Um aviso (warning, não error: o programa rodou). O compilador desconfiou: "o FazerSom do Cachorro oculta o de Animal".
  • O mesmo objeto deu duas respostas. Na linha 4, Animal bicho = rex; guarda o Rex numa variável do tipo Animal (todo cachorro é um animal, então pode). Por ela, saiu o som da base.

Sem virtual, o C# escolhe o método pelo tipo da variável, e não pelo objeto que está lá dentro. A variável bicho é Animal, então roda o FazerSom de Animal. Isso quase nunca é o que você quer: o Rex é um cachorro e deveria latir, não importa por qual variável você fale com ele.


virtual na base, override na filha

var rex = new Cachorro { Nome = "Rex" };
rex.FazerSom();

Animal bicho = rex;
bicho.FazerSom();

class Animal
{
    public string Nome { get; set; } = "";

    public virtual void FazerSom()
    {
        Console.WriteLine($"{Nome} faz um som qualquer.");
    }
}

class Cachorro : Animal
{
    public override void FazerSom()
    {
        Console.WriteLine($"{Nome}: Au au!");
    }
}
$ dotnet run virtual.cs
Rex: Au au!
Rex: Au au!          « agora o Rex late pelas duas variáveis
public virtual void FazerSom()
virtual vem depois do modificador de acesso. A base continua tendo uma versão completa do método (o "som qualquer"), mas avisa que as filhas podem trocá-la.
public override void FazerSom()
override diz que este método substitui o virtual da base. Nome, parâmetros, tipo de retorno e acesso têm de ser iguais aos da base.
bicho.FazerSom()
Com virtual/override, o C# olha para o objeto de verdade (um Cachorro) na hora de rodar, e não para o tipo da variável. Por isso o Rex late.

Sem aviso nenhum, e com o comportamento certo. Isso fica poderoso numa lista de animais diferentes:

Animal[] bichos =
{
    new Cachorro { Nome = "Rex" },
    new Gato { Nome = "Mia" },
    new Animal { Nome = "Nemo" },
};

foreach (Animal bicho in bichos)
{
    bicho.FazerSom();
}

class Animal
{
    public string Nome { get; set; } = "";

    public virtual void FazerSom()
    {
        Console.WriteLine($"{Nome} faz um som qualquer.");
    }
}

class Cachorro : Animal
{
    public override void FazerSom()
    {
        Console.WriteLine($"{Nome}: Au au!");
    }
}

class Gato : Animal
{
    public override void FazerSom()
    {
        Console.WriteLine($"{Nome}: Miau!");
    }
}
$ dotnet run lista.cs
Rex: Au au!
Mia: Miau!
Nemo faz um som qualquer.          « o Nemo é um Animal comum: usa a versão da base

O foreach tem uma linha só, bicho.FazerSom();, e cada objeto responde do seu jeito. Esse é o coração do polimorfismo, assunto da próxima aula.

Missão: o gato está fazendo "um som qualquer", e o compilador dá o aviso CS0114. Rode e leia o aviso. Depois, faça o FazerSom do Gato sobrescrever o da base, para a Mia miar sem aviso nenhum.

Animal[] bichos =
{
    new Cachorro { Nome = "Rex" },
    new Gato { Nome = "Mia" },
};

foreach (Animal bicho in bichos)
{
    bicho.FazerSom();
}

class Animal
{
    public string Nome { get; set; } = "";

    public virtual void FazerSom()
    {
        Console.WriteLine($"{Nome} faz um som qualquer.");
    }
}

class Cachorro : Animal
{
    public override void FazerSom()
    {
        Console.WriteLine($"{Nome}: Au au!");
    }
}

class Gato : Animal
{
    public void FazerSom()
    {
        Console.WriteLine($"{Nome}: Miau!");
    }
}
Animal[] bichos =
{
    new Cachorro { Nome = "Rex" },
    new Gato { Nome = "Mia" },
};

foreach (Animal bicho in bichos)
{
    bicho.FazerSom();
}

class Animal
{
    public string Nome { get; set; } = "";

    public virtual void FazerSom()
    {
        Console.WriteLine($"{Nome} faz um som qualquer.");
    }
}

class Cachorro : Animal
{
    public override void FazerSom()
    {
        Console.WriteLine($"{Nome}: Au au!");
    }
}

class Gato : Animal
{
    public override void FazerSom()
    {
        Console.WriteLine($"{Nome}: Miau!");
    }
}

O aviso CS0114 e a palavra new

E se a base tem virtual, mas a filha esquece o override? O compilador avisa de novo, agora com o CS0114:

Animal bicho = new Cachorro { Nome = "Rex" };
bicho.FazerSom();

class Animal
{
    public string Nome { get; set; } = "";

    public virtual void FazerSom()
    {
        Console.WriteLine($"{Nome} faz um som qualquer.");
    }
}

class Cachorro : Animal
{
    public void FazerSom()
    {
        Console.WriteLine($"{Nome}: Au au!");
    }
}
$ dotnet run cs0114.cs
/home/ana/cs/cs0114.cs(16,17): warning CS0114: "Cachorro.FazerSom()" oculta o membro herdado "Animal.FazerSom()". Para que o membro atual substitua essa implementação, adicione a palavra-chave override. Caso contrário, adicione a palavra-chave new.
Rex faz um som qualquer.

A mensagem dá as duas saídas. Em inglês: 'Cachorro.FazerSom()' hides inherited member 'Animal.FazerSom()'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.

  • override: "quero substituir". É a resposta certa em 99% dos casos.
  • new: "quero mesmo um método separado, com o mesmo nome, que esconde o da base". O aviso some, mas o comportamento continua o do exemplo de cima: pela variável Animal, roda o da base.
$ dotnet run com-new.cs          « o mesmo programa, com public new void FazerSom()
Rex faz um som qualquer.          « sem aviso, mas ainda o som da base

Aqui, new não é o new que cria objetos: é a mesma palavra com outro papel, o de modificador que diz "escondo de propósito". Ele existe para casos raros, como quando a base vem de uma biblioteca e ganhou, numa versão nova, um método com o mesmo nome de um seu. Se você não tem um motivo assim, use override.

Na baseNa filhaCompiladorAnimal bicho = rex; bicho.FazerSom();
void FazerSom()void FazerSom()Aviso CS0108Versão da base
virtual void FazerSom()void FazerSom()Aviso CS0114Versão da base
virtual void FazerSom() (ou não)new void FazerSom()Sem avisoVersão da base
virtual void FazerSom()override void FazerSom()Sem avisoVersão da filha
void FazerSom()override void FazerSom()Erro CS0506(não compila)

Sobrescrevendo ToString()

Você já usa um método virtual há muito tempo sem saber: o ToString() de object, a classe base de todas (veja Herança). Console.WriteLine(obj) e a interpolação $"{obj}" chamam o ToString() do objeto. O padrão só mostra o nome da classe; com override, você decide o texto:

var rex = new Cachorro { Nome = "Rex", Raca = "vira-lata" };
Console.WriteLine(rex);
Console.WriteLine($"Meu cachorro: {rex}");

class Cachorro
{
    public string Nome { get; set; } = "";
    public string Raca { get; set; } = "";

    public override string ToString()
    {
        return $"{Nome} ({Raca})";
    }
}
$ dotnet run tostring.cs
Rex (vira-lata)
Meu cachorro: Rex (vira-lata)

O Cachorro não tem : Animal aqui, e mesmo assim pode fazer override: ele herda de object, que declara public virtual string ToString(). Sem o override, a saída seria Cachorro. Mais em A Classe object.


Propriedades também

Propriedades podem ser virtual e receber override, do mesmo jeito que métodos:

Conta[] contas = { new Conta(), new ContaPremium() };
foreach (Conta c in contas)
{
    Console.WriteLine($"{c.GetType().Name}: tarifa de {c.Tarifa:C}");
}

class Conta
{
    public virtual decimal Tarifa => 12.90m;
}

class ContaPremium : Conta
{
    public override decimal Tarifa => 0m;
}
$ dotnet run tarifa.cs
Conta: tarifa de R$ 12,90
ContaPremium: tarifa de R$ 0,00

Reaproveitando a base: base.Metodo()

A versão da filha não precisa jogar fora a da base. Com base.Metodo() (da aula base), ela faz o que a base fazia e acrescenta o que é dela. O salário de um gerente é o salário de qualquer funcionário mais um bônus:

Funcionario[] equipe =
{
    new Funcionario { Nome = "Bruno", SalarioBase = 2500m },
    new Gerente { Nome = "Carla", SalarioBase = 4000m, Bonus = 1500m },
};

foreach (Funcionario f in equipe)
{
    Console.WriteLine($"{f.Nome}: {f.Salario():C}");
}

class Funcionario
{
    public string Nome { get; set; } = "";
    public decimal SalarioBase { get; set; }

    public virtual decimal Salario()
    {
        return SalarioBase;
    }
}

class Gerente : Funcionario
{
    public decimal Bonus { get; set; }

    public override decimal Salario()
    {
        return base.Salario() + Bonus;
    }
}
$ dotnet run salario.cs
Bruno: R$ 2.500,00
Carla: R$ 5.500,00

Se amanhã a regra do salário de todos mudar (descontar o vale-transporte, por exemplo), só Funcionario.Salario() muda, e o gerente acompanha.


Regras do override

O override tem de ser uma troca exata. O compilador confere cada detalhe:

RegraSe quebrar
A base tem de ser virtual (ou abstract, ou já override)CS0506
Tem de existir um método com o mesmo nome e os mesmos parâmetros na baseCS0115
O tipo de retorno tem de ser o mesmoCS0508
O modificador de acesso tem de ser o mesmoCS0507
Um método virtual não pode ser private (a filha nem o enxergaria)CS0621
Métodos static não podem ser virtual nem override(são da classe, não do objeto)

sealed override: daqui para baixo, ninguém troca

Uma filha pode fechar a porta para as netas com sealed override ("sobrescrita lacrada"):

class Animal
{
    public virtual void FazerSom() { }
}

class Cachorro : Animal
{
    public sealed override void FazerSom() { }
}

class Filhote : Cachorro
{
    public override void FazerSom() { }
}
$ dotnet run lacrado.cs
/home/ana/cs/lacrado.cs(13,26): error CS0239: "Filhote.FazerSom()": não é possível substituir o membro herdado "Cachorro.FazerSom()" porque ele é sealed

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

Para lacrar a classe inteira, veja sealed.


Quando usar virtual

  • Marque virtual só o que faz sentido variar de uma filha para outra: o som do animal, a tarifa da conta, o cálculo do salário.
  • Métodos que não devem mudar (a regra de saque, por exemplo) ficam sem virtual. Ao contrário do Java, em C# os métodos não são virtual por padrão: você escolhe o que abre.
  • Quando a base nem tem uma versão que faça sentido ("o som de um animal qualquer"?), o caminho é um método abstract, que obriga cada filha a escrever o seu. Veja Classes Abstratas.

Quando dá errado

warning CS0108 / CS0114: "…" oculta o membro herdado "…"

O que aconteceu: a filha tem um método com o mesmo nome e os mesmos parâmetros da base, sem override. O programa roda, mas pela variável do tipo da base sai a versão da base.

Como resolver: deixe o método da base virtual (no CS0108) e escreva override na filha. Use new só se quiser mesmo dois métodos separados.

error CS0506: … não é possível substituir o membro herdado "…" porque ele não está marcado como virtual, abstract ou override
$ dotnet run cs0506.cs
/home/ana/cs/cs0506.cs(11,26): error CS0506: "Cachorro.FazerSom()": não é possível substituir o membro herdado "Animal.FazerSom()" porque ele não está marcado como virtual, abstract ou override

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que aconteceu: override na filha, mas a base não deixou trocar.

Como resolver: escreva virtual no método da base. Se a base não é sua (vem de uma biblioteca), ela não foi feita para ser trocada naquele ponto.

error CS0115: "…": não encontrado nenhum método adequado para substituição
$ dotnet run cs0115.cs          « public override void FazerBarulho(), mas a base tem FazerSom()
/home/ana/cs/cs0115.cs(11,26): error CS0115: "Cachorro.FazerBarulho()": não encontrado nenhum método adequado para substituição

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que aconteceu: o nome ou os parâmetros não batem com nenhum método da base. Quase sempre é erro de digitação.

Como resolver: copie a assinatura exata do método da base.

error CS0508 / CS0507: tipo de retorno ou acesso diferentes
$ dotnet run cs0508.cs          « public override string FazerSom(), mas a base devolve void
/home/ana/cs/cs0508.cs(11,28): error CS0508: "Cachorro.FazerSom()": tipo de retorno deve ser "void" para corresponder ao membro substituído "Animal.FazerSom()"

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
$ dotnet run cs0507.cs          « protected override, mas a base é public
/home/ana/cs/cs0507.cs(11,29): error CS0507: "Cachorro.FazerSom()": não é possível alterar modificadores de acesso ao substituir "public" membro herdado "Animal.FazerSom()"

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

Como resolver: o override repete o tipo de retorno e o modificador de acesso da base.

error CS0621: "…": membros virtuais ou abstratos não podem ser privados
$ dotnet run cs0621.cs          « private virtual void FazerSom()
/home/ana/cs/cs0621.cs(3,26): error CS0621: "Animal.FazerSom()": membros virtuais ou abstratos não podem ser privados

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

Como resolver: use public ou protected virtual.


Referência

EscritaSignificado
public virtual void M()Na base: as filhas podem trocar este método
public override void M()Na filha: substitui o método virtual da base
base.M()Dentro do override: chama a versão da base
public new void M()Na filha: método separado que esconde o da base (raro)
public sealed override void M()Substitui e impede que as netas troquem de novo
public override string ToString()O texto que Console.WriteLine(obj) mostra
public virtual decimal P => …;Propriedade que as filhas podem trocar

Missão final: o salário do gerente

Missão: a Carla é gerente, mas está recebendo como funcionária (R$ 4.000,00). Deixe o Salario() de Funcionario virtual e escreva, em Gerente, um override que devolve base.Salario() + Bonus.

Funcionario[] equipe =
{
    new Funcionario { Nome = "Bruno", SalarioBase = 2500m },
    new Gerente { Nome = "Carla", SalarioBase = 4000m, Bonus = 1500m },
};

foreach (Funcionario f in equipe)
{
    Console.WriteLine($"{f.Nome}: {f.Salario():C}");
}

class Funcionario
{
    public string Nome { get; set; } = "";
    public decimal SalarioBase { get; set; }

    public decimal Salario()
    {
        return SalarioBase;
    }
}

class Gerente : Funcionario
{
    public decimal Bonus { get; set; }

    // escreva o override aqui
}
Funcionario[] equipe =
{
    new Funcionario { Nome = "Bruno", SalarioBase = 2500m },
    new Gerente { Nome = "Carla", SalarioBase = 4000m, Bonus = 1500m },
};

foreach (Funcionario f in equipe)
{
    Console.WriteLine($"{f.Nome}: {f.Salario():C}");
}

class Funcionario
{
    public string Nome { get; set; } = "";
    public decimal SalarioBase { get; set; }

    public virtual decimal Salario()
    {
        return SalarioBase;
    }
}

class Gerente : Funcionario
{
    public decimal Bonus { get; set; }

    public override decimal Salario()
    {
        return base.Salario() + Bonus;
    }
}

Exercícios

1. Pode trocar

Qual palavra, no método da classe base, permite que as filhas o substituam?

2. Estou trocando

Qual palavra, no método da classe filha, diz que ele substitui o da base?

3. O aviso do override esquecido

A base tem virtual, mas a filha esqueceu o override. Qual é o código do aviso?

4. Animal bicho = new Gato(); e Gato faz override do FazerSom virtual. Qual versão roda em bicho.FazerSom()?

5. O que faz o modificador new em public new void FazerSom()?

6. Por que Console.WriteLine(rex) mostra Rex (vira-lata) depois de um override de ToString()?


Resumo

  • Um método com o mesmo nome na filha, sem mais nada, só esconde o da base (aviso CS0108 ou CS0114), e pela variável do tipo da base roda a versão da base.
  • virtual na base + override na filha: a versão da filha substitui a da base, e o C# escolhe pelo objeto de verdade, não pelo tipo da variável.
  • new (modificador) esconde de propósito, sem aviso. Use override, salvo motivo forte.
  • override de ToString() controla o que Console.WriteLine(obj) mostra. Propriedades também podem ser virtual.
  • base.Metodo() reaproveita a versão da base; sealed override impede novas trocas (CS0239).
  • O override copia nome, parâmetros, retorno e acesso da base: CS0506, CS0115, CS0508, CS0507.