C# Polimorfismo
Polimorfismo vem do grego e quer dizer "muitas formas". Em C#, é a capacidade de escrever um código que fala com um Animal e funciona com qualquer animal: cachorro, gato ou um bicho que ainda nem foi inventado. Cada objeto responde do seu jeito à mesma instrução. É o terceiro pilar da orientação a objetos, e ele junta tudo o que você viu em Herança e virtual e override.
- Que uma variável do tipo da base pode guardar um objeto de qualquer classe derivada.
- Qual método roda quando você chama um método por essa variável (o despacho dinâmico).
- O que o tipo da variável não deixa fazer (
CS1061). - Onde o polimorfismo brilha: métodos e arrays que aceitam qualquer animal, e os pagamentos da lojinha.
- Como voltar ao tipo da classe derivada: cast (
CS0266,InvalidCastException),iscom padrão,aseswitchpor tipo.
A ideia: "traga o seu animal"
A clínica veterinária do bairro tem uma placa: "Consultas para animais". A recepcionista não pergunta "é cachorro?" antes de marcar. Ela marca para qualquer animal, e o veterinário faz sempre a mesma coisa: "deixa eu ouvir você". O cachorro late, o gato mia, o papagaio fala. A instrução é uma só; a resposta depende de quem está na mesa.
Em código:
- A placa "para animais" é um parâmetro (ou uma variável) do tipo
Animal. - "Deixa eu ouvir você" é a chamada
paciente.FazerSom(). - Cada um responder do seu jeito é o
overridede cada classe.
Uma variável Animal guardando um Cachorro
Todos os exemplos desta aula usam estas três classes, já conhecidas da aula anterior. O Cachorro ganhou um método só dele, BuscarBolinha:
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!");
}
public void BuscarBolinha()
{
Console.WriteLine($"{Nome} trouxe a bolinha.");
}
}
class Gato : Animal
{
public override void FazerSom()
{
Console.WriteLine($"{Nome}: Miau!");
}
}
Agora, o programa (as classes vêm logo abaixo, no mesmo arquivo):
Animal bicho = new Cachorro { Nome = "Rex" };
bicho.FazerSom();
bicho = new Gato { Nome = "Mia" };
bicho.FazerSom();
$ dotnet run variavel.cs Rex: Au au! Mia: Miau!
- Animal bicho = new Cachorro { … };
- A variável é do tipo
Animal, e o objeto é umCachorro. Pode, porque todo cachorro é um animal. O C# faz essa conversão sozinho, sem você pedir. - bicho.FazerSom();
- O Rex late: roda o
overridedoCachorro. - bicho = new Gato { … };
- A mesma variável agora guarda um gato. Também pode: todo gato é um animal.
- bicho.FazerSom();
- A mesma linha de antes, e agora a Mia mia. A instrução não mudou; o objeto mudou.
O contrário não vale: Cachorro rex = new Animal(); não compila, porque nem todo animal é um cachorro.
Qual método roda? O despacho dinâmico
Quando o programa chega em bicho.FazerSom(), existem duas perguntas, e o C# responde cada uma num momento diferente:
| Pergunta | Quem responde | Olhando o quê |
|---|---|---|
| Esse método existe? Posso chamá-lo? | O compilador, antes de rodar | O tipo da variável (Animal) |
| Qual versão do método roda? | O .NET, com o programa rodando | O tipo do objeto (Cachorro ou Gato), se o método é virtual |
Escolher a versão com o programa já rodando, pelo objeto de verdade, se chama despacho dinâmico. É o que virtual e override ligam. Sem virtual, a escolha é feita pelo tipo da variável, como você viu no aviso CS0108 da aula anterior.
O que o tipo da variável não deixa fazer
A primeira pergunta é a que pega quem está começando. O Rex sabe buscar a bolinha, mas, pela variável Animal, o compilador não sabe que ali dentro há um cachorro:
Animal bicho = new Cachorro { Nome = "Rex" };
bicho.BuscarBolinha();
$ dotnet run cs1061.cs /home/ana/cs/cs1061.cs(2,7): error CS1061: ‘Animal’ não contém uma definição para "BuscarBolinha" e não foi possível encontrar nenhum método de extensão "BuscarBolinha" que aceite um primeiro argumento do tipo ‘Animal’ (você está se esquecendo de usar uma diretiva ou uma referência de assembly?) Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
"Animal não contém uma definição": o compilador olhou o tipo da variável, e Animal não tem BuscarBolinha. Faz sentido: na linha 2, o bicho poderia ser um gato (ele acabou de ser um, no exemplo anterior). Pela variável Animal, você só usa o que todo animal tem. Mais abaixo você vê como perguntar "é cachorro?" antes de pedir a bolinha.
Onde o polimorfismo brilha
Um método para qualquer animal
Consultar(new Cachorro { Nome = "Rex" });
Consultar(new Gato { Nome = "Mia" });
void Consultar(Animal paciente)
{
Console.WriteLine($"Veterinário: agora é a vez de {paciente.Nome}.");
paciente.FazerSom();
}
$ dotnet run veterinario.cs Veterinário: agora é a vez de Rex. Rex: Au au! Veterinário: agora é a vez de Mia. Mia: Miau!
O Consultar foi escrito uma vez, para Animal, e atende cachorros e gatos. Se a Ana criar class Papagaio : Animal amanhã, o Consultar atende papagaios sem mudar uma vírgula. Isso é o principal ganho do polimorfismo: o código que usa os animais não precisa conhecer cada tipo de animal.
Um array com animais diferentes
Um array do tipo Animal[] guarda qualquer mistura de animais, e um foreach trata todos do mesmo jeito. Você viu isso na aula anterior. Agora, experimente acrescentar um bicho novo:
Missão: crie a classe Vaca, que herda de Animal e faz override do FazerSom mostrando o nome seguido de : Muuu!. Depois, acrescente new Vaca { Nome = "Mimosa" } no array. Repare que o foreach não muda.
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!");
}
}
Animal[] bichos =
{
new Cachorro { Nome = "Rex" },
new Gato { Nome = "Mia" },
new Vaca { Nome = "Mimosa" },
};
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!");
}
}
class Vaca : Animal
{
public override void FazerSom()
{
Console.WriteLine($"{Nome}: Muuu!");
}
}
Um caso de verdade: os pagamentos da lojinha
Bichos são bons para entender; agora, algo que a Ana usa de verdade. A lojinha aceita Pix, cartão e boleto, e cada um cobra uma taxa diferente. Sem polimorfismo, o código ficaria cheio de "se for cartão, faça isso; se for boleto, faça aquilo". Com polimorfismo, cada forma de pagamento sabe a sua taxa:
Pagamento[] pagamentos =
{
new Pix { Valor = 100m },
new Cartao { Valor = 100m },
new Boleto { Valor = 100m },
};
decimal totalTaxas = 0m;
foreach (Pagamento p in pagamentos)
{
decimal taxa = p.Taxa();
totalTaxas += taxa;
Console.WriteLine($"{p.Descricao,-8} {p.Valor:C} taxa {taxa:C}");
}
Console.WriteLine($"Total de taxas: {totalTaxas:C}");
class Pagamento
{
public decimal Valor { get; set; }
public virtual string Descricao => "Dinheiro";
public virtual decimal Taxa()
{
return 0m;
}
}
class Pix : Pagamento
{
public override string Descricao => "Pix";
}
class Cartao : Pagamento
{
public override string Descricao => "Cartão";
public override decimal Taxa()
{
return Valor * 0.03m;
}
}
class Boleto : Pagamento
{
public override string Descricao => "Boleto";
public override decimal Taxa()
{
return 3.50m;
}
}
$ dotnet run pagamento.cs Pix R$ 100,00 taxa R$ 0,00 Cartão R$ 100,00 taxa R$ 3,00 Boleto R$ 100,00 taxa R$ 3,50 Total de taxas: R$ 6,50
- Pagamento[] pagamentos
- Um array do tipo da base, com um objeto de cada classe derivada.
- p.Taxa()
- Uma linha só. O Pix usa a versão da base (0), o cartão cobra 3% do valor, o boleto cobra R$ 3,50 fixos.
- {p.Descricao,-8}
- Mostra a descrição ocupando 8 colunas, alinhada à esquerda, para a tabela ficar reta (veja Formatar Números).
- class Pix : Pagamento
- Não fez
overridedeTaxa: herdou a versão da base, que devolve 0.
Quando o banco da Ana lançar o "Pix parcelado", ela cria uma classe nova com a taxa dele. O foreach, o total e o resto do programa não mudam.
Voltando para o tipo da classe derivada
Às vezes você tem um Animal e precisa de algo que só o Cachorro tem, como o BuscarBolinha. Há quatro jeitos, do mais arriscado ao mais seguro.
1. Atribuir direto: não compila
Animal bicho = new Cachorro { Nome = "Rex" };
Cachorro rex = bicho;
rex.BuscarBolinha();
$ dotnet run cs0266.cs /home/ana/cs/cs0266.cs(2,16): error CS0266: Não é possível converter implicitamente tipo "Animal" em "Cachorro". Existe uma conversão explícita (há uma conversão ausente?) Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O compilador não deixa, porque um Animal pode não ser um cachorro. A mensagem dá a dica: "existe uma conversão explícita", isto é, você pode pedir a conversão com todas as letras.
2. O cast: (Cachorro)bicho
Animal bicho = new Cachorro { Nome = "Rex" };
Cachorro rex = (Cachorro)bicho;
rex.BuscarBolinha();
$ dotnet run cast.cs Rex trouxe a bolinha.
O (Cachorro) antes da variável é um cast (conversão explícita, como em Conversão de Tipos). Você está dizendo ao compilador: "confie em mim, aqui dentro tem um cachorro". Ele confia, e o .NET confere quando o programa roda. Se você estiver errado:
Animal bicho = new Gato { Nome = "Mia" };
Cachorro rex = (Cachorro)bicho;
rex.BuscarBolinha();
$ dotnet run invalid.cs Unhandled exception. System.InvalidCastException: Unable to cast object of type 'Gato' to type 'Cachorro'. at Program.<Main>$(String[] args) in /home/ana/cs/invalid.cs:line 2 $ echo $? 134
Compilou, mas o programa caiu na linha 2: "não é possível converter um objeto do tipo Gato para o tipo Cachorro". Use o cast só quando você tem certeza do tipo.
3. is com padrão: pergunte antes C# 7
O jeito seguro e moderno é perguntar e converter de uma vez só:
Animal[] bichos = { new Cachorro { Nome = "Rex" }, new Gato { Nome = "Mia" } };
foreach (Animal bicho in bichos)
{
bicho.FazerSom();
if (bicho is Cachorro cachorro)
{
cachorro.BuscarBolinha();
}
}
$ dotnet run is.cs Rex: Au au! Rex trouxe a bolinha. Mia: Miau! « a Mia não é cachorro: o if foi pulado
- bicho is Cachorro cachorro
- Pergunta "o
bichoé umCachorro?". Se for, devolvetruee cria a variávelcachorro, já do tipoCachorro, apontando para o mesmo objeto. Se não for, devolvefalse, e nada quebra. - cachorro.BuscarBolinha();
- Dentro do
if,cachorroé umCachorro, então o compilador deixa chamarBuscarBolinha.
4. as: converta ou devolva null
O operador as tenta converter e, se não der, devolve null em vez de lançar exceção:
Animal bicho = new Gato { Nome = "Mia" };
Cachorro? rex = bicho as Cachorro;
Console.WriteLine(rex is null ? "Não é cachorro." : $"É o {rex.Nome}.");
$ dotnet run as.cs Não é cachorro.
O ? em Cachorro? avisa que a variável pode ficar null (veja Referências Anuláveis). Você tem de conferir antes de usar. Hoje, o is com padrão faz o mesmo em menos linhas, e é o preferido.
Bônus: switch por tipo
Com vários tipos para separar, uma expressão switch deixa tudo num lugar só:
Animal[] bichos = { new Cachorro { Nome = "Rex" }, new Gato { Nome = "Mia" }, new Animal { Nome = "Nemo" } };
foreach (Animal bicho in bichos)
{
string tipo = bicho switch
{
Cachorro => "cachorro",
Gato => "gato",
_ => "outro bicho",
};
Console.WriteLine($"{bicho.Nome} é {tipo}.");
}
$ dotnet run switch.cs Rex é cachorro. Mia é gato. Nemo é outro bicho.
Mais formas de perguntar pelo tipo em Pattern Matching.
if (x is Cachorro) … else if (x is Gato) … para decidir o que fazer, quase sempre o certo é um método virtual na base com um override em cada classe. Foi o que os pagamentos fizeram: ninguém perguntou "é boleto?".Qual usar?
| Situação | Use |
|---|---|
| Todos os tipos fazem a mesma coisa, cada um do seu jeito | Método virtual + override (polimorfismo de verdade) |
| Só alguns tipos têm um recurso extra | if (x is Cachorro c) { … } |
| Escolher um valor diferente para cada tipo | x switch { Cachorro => …, Gato => …, _ => … } |
| Você tem certeza absoluta do tipo | Cast: (Cachorro)x |
Código antigo, ou quer null quando não é | x as Cachorro |
override, é o "de execução": a escolha acontece com o programa rodando, pelo objeto.Quando dá errado
O que aconteceu: você chamou, por uma variável do tipo da base, um método que só a classe derivada tem.
Como resolver: se todos os animais deveriam ter o método, coloque-o (virtual) na base. Se não, pergunte antes: if (bicho is Cachorro c) c.BuscarBolinha();.
O que aconteceu: você guardou uma variável da base numa variável da derivada.
Como resolver: use is com padrão (seguro) ou um cast (Cachorro) (se tem certeza).
O que aconteceu: o cast compilou, mas o objeto não era daquele tipo. O programa para com o código de saída 134.
Como resolver: troque o cast por if (bicho is Cachorro rex) { … }.
O que aconteceu: o método da base não é virtual, ou a filha esqueceu o override. Confira os avisos CS0108 e CS0114.
Como resolver: virtual na base, override na filha. Veja virtual e override.
Referência
| Escrita | Significado |
|---|---|
Animal a = new Cachorro(); | Variável da base guardando um objeto da derivada (automático) |
void M(Animal a) { … } | Método que aceita qualquer animal |
Animal[] bichos = { … }; | Array com animais de vários tipos |
(Cachorro)a | Cast: converte ou lança InvalidCastException |
a is Cachorro c C# 7 | Pergunta e, se for, cria c do tipo Cachorro |
a as Cachorro | Converte ou devolve null |
a switch { Cachorro => …, _ => … } | Escolhe um valor pelo tipo do objeto |
Missão final: as taxas da lojinha
Missão: o cartão e o boleto estão saindo sem taxa (Total de taxas: R$ 0,00). Escreva, na classe Cartao, um override de Taxa() que devolve 3% do valor (Valor * 0.03m) e, na classe Boleto, um que devolve 3.50m. O foreach não muda.
Pagamento[] pagamentos =
{
new Pix { Valor = 100m },
new Cartao { Valor = 100m },
new Boleto { Valor = 100m },
};
decimal totalTaxas = 0m;
foreach (Pagamento p in pagamentos)
{
decimal taxa = p.Taxa();
totalTaxas += taxa;
Console.WriteLine($"{p.Descricao,-8} {p.Valor:C} taxa {taxa:C}");
}
Console.WriteLine($"Total de taxas: {totalTaxas:C}");
class Pagamento
{
public decimal Valor { get; set; }
public virtual string Descricao => "Dinheiro";
public virtual decimal Taxa()
{
return 0m;
}
}
class Pix : Pagamento
{
public override string Descricao => "Pix";
}
class Cartao : Pagamento
{
public override string Descricao => "Cartão";
}
class Boleto : Pagamento
{
public override string Descricao => "Boleto";
}
Pagamento[] pagamentos =
{
new Pix { Valor = 100m },
new Cartao { Valor = 100m },
new Boleto { Valor = 100m },
};
decimal totalTaxas = 0m;
foreach (Pagamento p in pagamentos)
{
decimal taxa = p.Taxa();
totalTaxas += taxa;
Console.WriteLine($"{p.Descricao,-8} {p.Valor:C} taxa {taxa:C}");
}
Console.WriteLine($"Total de taxas: {totalTaxas:C}");
class Pagamento
{
public decimal Valor { get; set; }
public virtual string Descricao => "Dinheiro";
public virtual decimal Taxa()
{
return 0m;
}
}
class Pix : Pagamento
{
public override string Descricao => "Pix";
}
class Cartao : Pagamento
{
public override string Descricao => "Cartão";
public override decimal Taxa()
{
return Valor * 0.03m;
}
}
class Boleto : Pagamento
{
public override string Descricao => "Boleto";
public override decimal Taxa()
{
return 3.50m;
}
}
Exercícios
1. Pergunte antes
Complete o if para perguntar se bicho é um Gato e, se for, criar a variável g: if (bicho ___)
2. O cast
Escreva o cast que converte a variável bicho para Cachorro (sem ponto e vírgula).
3. Muitas formas
Qual palavra, de origem grega, quer dizer "muitas formas"?
4. Em Animal bicho = new Gato();, quem decide se bicho.FazerSom() pode ser chamado, e quem decide qual versão roda?
5. O que acontece com Cachorro rex = (Cachorro)bicho; quando bicho guarda um Gato?
6. Qual é a maior vantagem de um método Consultar(Animal paciente)?
Resumo
- Uma variável (ou parâmetro, ou array) do tipo da base guarda objetos de qualquer classe derivada:
Animal bicho = new Cachorro();. - O tipo da variável diz o que pode ser chamado (
CS1061); o tipo do objeto diz qual versão roda, quando o método évirtual(despacho dinâmico). - Código escrito para a base (
Consultar(Animal), oforeachdos pagamentos) funciona com qualquer derivada, até com as que ainda vão ser criadas. - Para usar algo só da derivada:
if (x is Cachorro c)(seguro),(Cachorro)x(pode lançarInvalidCastException),x as Cachorro(pode darnull) ouswitchpor tipo. - Muitos
if (x is …)são sinal de que falta um métodovirtualna base.