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".
- Por que só escrever um método com o mesmo nome na filha não basta (o aviso
CS0108). virtualna base eoverridena filha.- Que, com
override, o método certo roda até numa variável do tipo da base. - O aviso
CS0114e a palavranew: esconder × sobrescrever. - Sobrescrever
ToString()e propriedades, e reaproveitar a base combase.Metodo(). sealed overridee os errosCS0506,CS0115,CS0508,CS0507,CS0621eCS0239.
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ãoerror: o programa rodou). O compilador desconfiou: "oFazerSomdoCachorrooculta o deAnimal". - O mesmo objeto deu duas respostas. Na linha 4,
Animal bicho = rex;guarda o Rex numa variável do tipoAnimal(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()
virtualvem 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()
overridediz que este método substitui ovirtualda 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 (umCachorro) 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ávelAnimal, 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 base | Na filha | Compilador | Animal bicho = rex; bicho.FazerSom(); |
|---|---|---|---|
void FazerSom() | void FazerSom() | Aviso CS0108 | Versão da base |
virtual void FazerSom() | void FazerSom() | Aviso CS0114 | Versão da base |
virtual void FazerSom() (ou não) | new void FazerSom() | Sem aviso | Versão da base |
virtual void FazerSom() | override void FazerSom() | Sem aviso | Versã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:
| Regra | Se 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 base | CS0115 |
| O tipo de retorno tem de ser o mesmo | CS0508 |
| O modificador de acesso tem de ser o mesmo | CS0507 |
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
virtualsó 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ãovirtualpor 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
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.
$ 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.
$ 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.
$ 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.
$ 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
| Escrita | Significado |
|---|---|
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
CS0108ouCS0114), e pela variável do tipo da base roda a versão da base. virtualna base +overridena 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. Useoverride, salvo motivo forte.overridedeToString()controla o queConsole.WriteLine(obj)mostra. Propriedades também podem servirtual.base.Metodo()reaproveita a versão da base;sealed overrideimpede novas trocas (CS0239).- O
overridecopia nome, parâmetros, retorno e acesso da base:CS0506,CS0115,CS0508,CS0507.