Polimorfismo: Um Comando, Muitos Comportamentos

Polimorfismo: Um Comando, Muitos Comportamentos

Um mesmo comando produzindo comportamentos diferentes conforme o objeto: o polimorfismo. A aula cobre virtual e override, o despacho em tempo de execução, a palavra base para reaproveitar o comportamento herdado, e por que exigir override explícito evita substituições acidentais.
Linguagem C#

• • 11 min de leitura

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

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.

Comentários

Mais em Linguagem C#

Encapsulamento: Objetos que Protegem seu Estado
Encapsulamento: Objetos que Protegem seu Estado

O princípio que fecha a fase de objetos: esconder o interior e expor apenas o…

Recursos Modernos: Pattern Matching, Tuplas e Records
Recursos Modernos: Pattern Matching, Tuplas e Records

Três recursos do C# moderno que encurtam o código do dia a dia: o pattern…

Jogos em C#: Unity e MonoGame
Jogos em C#: Unity e MonoGame

Como o C# se aplica ao desenvolvimento de jogos: a Unity, que o usa como…