A herança nos deu famílias de objetos que compartilham características e comportamentos. Mas há uma limitação evidente no que vimos: os comportamentos herdados são idênticos para todos os descendentes. Um cachorro e um gato, ambos herdando um método EmitirSom de Animal, emitiriam o mesmo som genérico — o que é absurdo, pois o cachorro late e o gato mia. Precisamos que cada tipo derivado possa personalizar um comportamento herdado, dando-lhe sua própria versão. E, mais poderoso ainda, precisamos que o programa, ao mandar um animal "emitir som", produza automaticamente o som correto para o tipo real daquele animal, sem que precisemos verificar manualmente de qual tipo se trata. Essa capacidade — um mesmo comando produzindo comportamentos diferentes conforme o tipo real do objeto — chama-se polimorfismo, e é um dos conceitos mais elegantes e poderosos de toda a orientação a objetos. Esta aula o desvenda.
A palavra: muitas formas
O termo "polimorfismo" vem do grego e significa, literalmente, "muitas formas". No contexto dos objetos, ele descreve a capacidade de um mesmo comando ou método assumir "muitas formas" de comportamento, conforme o tipo específico do objeto sobre o qual atua. É um nome intimidante para uma ideia que, uma vez vista em ação, é bastante intuitiva: mandar cada animal "fazer seu som" e cada um fazer o som que lhe é próprio, automaticamente. Vamos construí-la em duas etapas: primeiro, permitir que cada classe personalize um comportamento herdado; segundo, ver o programa escolher o comportamento certo sozinho.
Etapa 1: virtual e override, personalizando comportamentos
Para que uma classe derivada possa fornecer sua própria versão de um método herdado, duas coisas precisam acontecer. A classe base precisa permitir que o método seja personalizado, marcando-o com a palavra virtual (que significa "isto pode ser redefinido pelas classes derivadas"). E a classe derivada precisa fornecer sua versão, marcando-a com a palavra override (que significa "estou substituindo o método herdado"). Veja:
class Animal
{
public string Nome { get; set; }
// 'virtual' permite que as classes derivadas redefinam este método.
public virtual void EmitirSom()
{
Console.WriteLine($"{Nome} faz um som genérico.");
}
}
class Cachorro : Animal
{
// 'override' fornece a versão específica do cachorro.
public override void EmitirSom()
{
Console.WriteLine($"{Nome} faz: Au au!");
}
}
class Gato : Animal
{
public override void EmitirSom()
{
Console.WriteLine($"{Nome} faz: Miau!");
}
}
Agora cada tipo tem seu próprio comportamento para EmitirSom. O virtual na base abre a porta; o override na derivada a atravessa, substituindo o comportamento genérico pelo específico:
Cachorro c = new Cachorro { Nome = "Rex" };
Gato g = new Gato { Nome = "Mimi" };
c.EmitirSom(); // Rex faz: Au au!
g.EmitirSom(); // Mimi faz: Miau!
Cada objeto executa a sua versão do método. O Cachorro late, o Gato mia — cada um personalizou o comportamento herdado. Isso já é útil, mas o verdadeiro poder do polimorfismo se revela na próxima etapa, quando tratamos objetos de tipos diferentes de maneira uniforme.
Etapa 2: o polimorfismo em ação
Aqui está a ideia genuinamente poderosa. Como um Cachorro é um Animal (herança!), podemos guardar um cachorro numa variável do tipo Animal. E, mais interessante, podemos ter uma coleção de Animal que contenha, na verdade, objetos de tipos diferentes — cachorros e gatos misturados —, tratando todos uniformemente como animais:
// Uma lista de Animal que contém objetos de tipos DIFERENTES:
List<Animal> animais = new()
{
new Cachorro { Nome = "Rex" },
new Gato { Nome = "Mimi" },
new Cachorro { Nome = "Bob" }
};
// Mandamos cada animal emitir som, sem verificar de que tipo é:
foreach (Animal animal in animais)
{
animal.EmitirSom();
}
// Saída:
// Rex faz: Au au!
// Mimi faz: Miau!
// Bob faz: Au au!
Observe atentamente o que aconteceu, pois é a essência do polimorfismo. A variável do laço é do tipo Animal, e chamamos animal.EmitirSom() de maneira uniforme para todos — sem verificar se cada um é cachorro ou gato. E, ainda assim, cada objeto executou o som correto para o seu tipo real: os cachorros latiram, o gato miou. O programa descobriu sozinho, para cada objeto, qual versão do método chamar, com base no tipo real daquele objeto — não no tipo da variável (Animal), mas no tipo do objeto que ela de fato guarda (Cachorro ou Gato). Esse é o polimorfismo: um mesmo comando (animal.EmitirSom()) produzindo comportamentos diferentes conforme o tipo real de cada objeto, automaticamente.
O valor disso é imenso. Imagine que a alternativa fosse verificar manualmente o tipo de cada animal com uma cadeia de if ("se for cachorro, late; se for gato, mia..."). Essa abordagem seria trabalhosa, repetitiva, e — pior — frágil: cada novo tipo de animal exigiria adicionar mais um if em todos os lugares que tratam animais. Com o polimorfismo, você simplesmente manda EmitirSom(), e cada objeto sabe o que fazer. Adicionar um novo tipo de animal (um Passaro que faz "Piu piu!") não exige mudar nada no código que percorre a lista — basta criar a nova classe com seu override, e ela se encaixa automaticamente. Essa capacidade de estender o programa sem modificar o código existente é uma das maiores virtudes da orientação a objetos, e o polimorfismo é seu principal veículo.
A segurança do override obrigatório
Vale notar um cuidado que o C# impõe e que previne erros. Para redefinir um método, você é obrigado a escrever override explicitamente, e a base é obrigada a marcá-lo como virtual. Se você tentar redefinir um método sem override, ou marcar como override um método que não existe na base, o C# reclamará. Essa exigência garante que a redefinição de comportamentos seja sempre intencional e explícita — você não redefine um método por acidente, nem pensa estar redefinindo algo que na verdade não corresponde a nada na base. É mais um exemplo da filosofia do C# de tornar as intenções explícitas e detectar erros cedo, que temos visto por todo o curso.
Chamando o comportamento da base: base
Às vezes, ao personalizar um método, você quer acrescentar algo ao comportamento da base, não substituí-lo por completo. A palavra base (a mesma dos construtores, na aula anterior) permite chamar a versão original do método, a partir da versão redefinida:
class Cachorro : Animal
{
public override void EmitirSom()
{
base.EmitirSom(); // primeiro executa o som genérico da base...
Console.WriteLine($"{Nome} faz: Au au!"); // ...e depois acrescenta o seu
}
}
Aqui, base.EmitirSom() executa o comportamento genérico definido em Animal, e então o cachorro acrescenta o seu latido. É útil quando o comportamento da base é uma etapa que se quer preservar, complementando-a com o específico do derivado. Nem sempre você precisará disso — muitas vezes a versão derivada substitui inteiramente a da base —, mas é bom conhecer o recurso.
O mesmo comando passou a produzir comportamentos diferentes conforme o objeto que o recebe. Com virtual na base e override na derivada, cada classe personaliza uma operação sem mudar o nome pelo qual ela é invocada; e ao tratar objetos distintos pelo tipo comum, o C# escolhe em tempo de execução qual versão executar.
O efeito prático é notável: um laço escrito uma vez atende a qualquer número de tipos, inclusive os que ainda não existem. Acrescentar um novo caso passa a significar acrescentar uma classe, sem tocar no código que já funciona. A exigência explícita de override, longe de ser burocracia, impede que uma substituição aconteça por acidente.
Fontes e leituras recomendadas
- Polimorfismo em C# — a referência oficial sobre polimorfismo,
virtualeoverride; a base desta aula. - A palavra-chave virtual — como marcar um método como redefinível pelas classes derivadas.
- A palavra-chave override — como fornecer a versão específica de um método herdado.
- Chamando membros da classe base — o uso de
basepara acessar o comportamento original. - Despacho dinâmico (conceito) — como o programa escolhe a versão correta do método em tempo de execução.
Exercícios
Exercício 1
Crie uma classe base Forma com um método virtual double Area() que retorne 0, e duas classes derivadas, Circulo (com property Raio) e Retangulo (com Largura e Altura), cada uma redefinindo Area() com o cálculo correto usando override. Crie um objeto de cada e exiba suas áreas.
Ver resposta
✓ Resposta: Exemplo:
class Forma
{
public virtual double Area() => 0;
}
class Circulo : Forma
{
public double Raio { get; set; }
public override double Area() => 3.14159 * Raio * Raio;
}
class Retangulo : Forma
{
public double Largura { get; set; }
public double Altura { get; set; }
public override double Area() => Largura * Altura;
}
Circulo c = new Circulo { Raio = 2 };
Retangulo r = new Retangulo { Largura = 3, Altura = 4 };
Console.WriteLine(c.Area()); // ~12.57
Console.WriteLine(r.Area()); // 12
Cada derivada redefine Area() com sua fórmula própria.
Exercício 2
Usando as classes do exercício anterior, crie uma List<Forma> com um círculo e um retângulo, e percorra-a com um foreach chamando Area() para cada forma. Explique como o programa "sabe" chamar o cálculo correto de cada tipo, sem que você verifique de qual tipo se trata.
Ver resposta
✓ Resposta: Exemplo:
List<Forma> formas = new() { new Circulo { Raio = 2 }, new Retangulo { Largura = 3, Altura = 4 } };
foreach (Forma f in formas)
Console.WriteLine(f.Area());
O programa sabe chamar o cálculo correto porque, ao executar f.Area(), ele verifica o tipo real do objeto guardado na variável f (que é Circulo num caso e Retangulo no outro, embora a variável seja declarada como Forma) e chama a versão de Area() correspondente àquele tipo. Essa escolha é feita automaticamente pelo polimorfismo, com base no objeto real, sem que o código precise verificar manualmente de qual tipo se trata.
Exercício 3
No exercício 1, remova a palavra override de uma das classes derivadas e tente compilar. Observe o erro. Depois, deixe o override mas remova o virtual da base e observe o outro erro. Explique o que cada exigência do C# está protegendo.
Ver resposta
✓ Resposta: Sem override na derivada, o compilador acusa que o método esconde o membro herdado e exige a palavra override (ou outra indicação explícita) — isso protege contra redefinir um método por acidente sem declarar a intenção, ou contra pensar que se está personalizando quando na verdade se está criando um método novo e desconexo. Sem virtual na base (mas com override na derivada), o compilador acusa que não há método virtual correspondente para redefinir — isso protege contra tentar redefinir algo que a base não autorizou a ser redefinido, garantindo que só métodos deliberadamente marcados como personalizáveis o sejam. As duas exigências juntas garantem que a redefinição de comportamentos seja sempre intencional, explícita e coerente entre base e derivada.
Exercício 4
Crie uma classe base Notificacao com um método virtual void Enviar() que exiba uma mensagem genérica, e duas derivadas, Email e SMS, cada uma com sua própria versão de Enviar(). Coloque objetos das duas numa List<Notificacao> e envie todas com um foreach. Depois, acrescente uma terceira derivada (Push) e confirme que o código do foreach não precisou mudar.
Ver resposta
✓ Resposta: Exemplo:
class Notificacao { public virtual void Enviar() => Console.WriteLine("Notificação genérica."); }
class Email : Notificacao { public override void Enviar() => Console.WriteLine("Enviando e-mail."); }
class SMS : Notificacao { public override void Enviar() => Console.WriteLine("Enviando SMS."); }
class Push : Notificacao { public override void Enviar() => Console.WriteLine("Enviando push."); }
List<Notificacao> notificacoes = new() { new Email(), new SMS(), new Push() };
foreach (Notificacao n in notificacoes)
n.Enviar();
Ao acrescentar a classe Push com seu override, o foreach que envia todas as notificações não precisou de nenhuma alteração — ele continua chamando n.Enviar() uniformemente, e o polimorfismo cuida de executar a versão certa, inclusive a nova. Isso ilustra a extensibilidade: novos tipos se encaixam sem modificar o código que os utiliza.
Exercício 5
Sem código: explique, com suas palavras, o que é o polimorfismo e por que ele é descrito como "um comando, muitos comportamentos". Por que ele é preferível a uma cadeia de if que verifica manualmente o tipo de cada objeto?
Ver resposta
✓ Resposta: O polimorfismo é a capacidade de um mesmo comando produzir comportamentos diferentes conforme o tipo real do objeto sobre o qual atua. É descrito como "um comando, muitos comportamentos" porque você escreve uma única instrução uniforme (por exemplo, animal.EmitirSom()) e ela se comporta de "muitas formas" — late para um cachorro, mia para um gato — dependendo de qual objeto concreto a executa, sem que o comando mude. Ele é preferível a uma cadeia de if que verifica manualmente o tipo porque a cadeia de if é repetitiva, precisa ser mantida em todos os lugares que tratam aqueles objetos, e é frágil: cada novo tipo exige adicionar mais um if em cada um desses lugares. Com o polimorfismo, cada objeto "sabe" seu próprio comportamento, e adicionar um novo tipo não exige tocar no código que já usa os objetos — o novo tipo se encaixa automaticamente, tornando o programa mais fácil de estender e menos propenso a erros.