1. Polimorfismo na prática
Em programação, polimorfismo é tipo um controle remoto universal: você aperta o mesmo botão "Volume+", mas pode controlar a TV, o som ou o ar-condicionado — cada aparelho reage do seu jeito, mas a interface é a mesma! Do mesmo jeito, objetos de tipos diferentes podem reagir de formas diferentes à mesma chamada de método, se esse método foi definido no ancestral comum como virtual.
Em C#, o polimorfismo aparece quando uma variável do tipo classe base (ou interface) pode "guardar" um objeto de qualquer descendente dela, e as chamadas de métodos virtuais nessa variável vão executar a implementação específica — aquela que o próprio objeto definiu. Isso é a base de arquiteturas onde a lógica muda dinamicamente.
Isso serve pra alguma coisa fora dos livros?
Com certeza! Praticamente em qualquer projeto onde você trabalha com objetos diferentes, mas parecidos — de animais no zoológico até elementos de interface gráfica, de handlers de eventos até sistemas de documentos.
- Permite criar algoritmos universais — código que trabalha com entidades abstratas, sem se preocupar com detalhes da implementação concreta.
- Garante que dá pra expandir fácil — pode adicionar cem tipos novos de "animais", "figuras", "handlers", sem mexer no código que já existe.
- Reduz a dependência entre componentes do programa (isso é um dos tópicos importantes pra entrevistas e arquitetura!).
2. Sintaxe básica e como funciona
Bora relembrar e melhorar nossas classes Animal, Dog, Cat, pra mostrar o polimorfismo rolando. Já que começamos com o app "Zoológico Virtual", vamos deixar ele mais massa.
Classes base com métodos virtuais
public class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
}
// Método virtual — pode ser sobrescrito
public virtual void MakeSound()
{
Console.WriteLine($"{Name} faz algum som...");
}
}
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} diz: Au-au!");
}
}
public class Cat : Animal
{
public Cat(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} diz: Miau!");
}
}
Usando polimorfismo: exemplo com coleção de animais
Agora imagina que temos uma lista de animais — domésticos ou não — e queremos fazer todos eles "emitirem um som". Sem polimorfismo, ia ter que checar o tipo e escrever um monte de código repetido. Com polimorfismo — fica elegante e enxuto!
// Criando um array de animais de tipos diferentes
Animal[] animals = new Animal[]
{
new Dog("Bobik"),
new Cat("Murka"),
new Dog("Sharik"),
new Cat("Barsik"),
};
// Passando pelo array e pedindo pra cada um fazer um som
foreach (var animal in animals)
{
animal.MakeSound(); // Vai chamar o método de Dog ou Cat, não de Animal!
}
Resultado:
Bobik diz: Au-au!
Murka diz: Miau!
Sharik diz: Au-au!
Barsik diz: Miau!
Essa é a mágica: um código universal — resultados diferentes dependendo do tipo real do objeto.
Diagrama: como o polimorfismo funciona
Animal (classe base)
/ \
Dog Cat
Quando você chama animal.MakeSound(), onde Animal pode ser uma instância de Dog ou Cat, o .NET na hora da execução decide sozinho qual método MakeSound() tem que rodar.
3. Resolvendo tarefas típicas com polimorfismo
Exemplo 1: Lista universal, ações diferentes
Imagina que você tá fazendo um jogo. Tem uma classe base GameObject, e as derivadas — inimigos, aliados, obstáculos. Todos podem se mover, todos têm o método Update(), mas cada um faz do seu jeito.
public class GameObject
{
public virtual void Update() { }
}
public class Enemy : GameObject
{
public override void Update()
{
Console.WriteLine("O inimigo avança!");
}
}
public class Friend : GameObject
{
public override void Update()
{
Console.WriteLine("O aliado ajuda!");
}
}
GameObject[] objects = new GameObject[]
{
new Enemy(),
new Friend(),
new Enemy()
};
foreach (var obj in objects)
{
obj.Update();
}
// Vai mostrar:
// O inimigo avança!
// O aliado ajuda!
// O inimigo avança!
Exemplo 2: Passando objetos para métodos
Dá pra receber um parâmetro do tipo base e usar qualquer tipo derivado. Isso economiza muito trampo, principalmente se o objeto for ganhar novos filhos depois.
public static void FeedAnimal(Animal animal)
{
Console.Write($"{animal.Name}: ");
animal.MakeSound();
Console.WriteLine("E ganha comida.");
}
FeedAnimal(new Dog("Rex"));
FeedAnimal(new Cat("Sima"));
// Resultado:
// Rex: Rex diz: Au-au!
// E ganha comida.
// Sima: Sima diz: Miau!
// E ganha comida.
Repara: fazer um método desses sem polimorfismo seria um saco — teria que checar o tipo do animal, um monte de if e chamar os métodos certos na mão.
Observação importante: ligação em tempo de execução
O polimorfismo funciona porque as chamadas de métodos virtuais são feitas de forma dinâmica, ou seja, na hora que o programa tá rodando. Isso se chama late binding. Mesmo que a variável seja Animal, o método chamado vai ser o que tá no objeto real.
Se o método não for marcado como virtual, a chamada sempre vai pro código que tá na variável (não no objeto). Então não economiza no virtual se quiser flexibilidade!
4. Prática: expandindo nosso app
Imagina que você quer adicionar uma nova feature no seu zoológico virtual: agora cada animal tem que fazer não só MakeSound(), mas também se mover (Move()). Mas cada um faz isso de um jeito.
1. Adiciona um método virtual na classe base
public class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
}
public virtual void MakeSound()
{
Console.WriteLine($"{Name} faz algum som...");
}
public virtual void Move()
{
Console.WriteLine($"{Name} se move de um jeito não especificado...");
}
}
2. Nas classes derivadas, implementa Move() do seu jeito
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} diz: Au-au!");
}
public override void Move()
{
Console.WriteLine($"{Name} corre atrás do graveto.");
}
}
public class Cat : Animal
{
public Cat(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} diz: Miau!");
}
public override void Move()
{
Console.WriteLine($"{Name} anda de mansinho com as patinhas.");
}
}
3. Usa os dois métodos na coleção
Animal[] animals = new Animal[]
{
new Dog("Bim"),
new Cat("Lúcia")
};
foreach (var animal in animals)
{
animal.MakeSound();
animal.Move();
}
Resultado:
Bim diz: Au-au!
Bim corre atrás do graveto.
Lúcia diz: Miau!
Lúcia anda de mansinho com as patinhas.
Em apps reais, esse jeito permite criar módulos super poderosos e reaproveitáveis. Em jogos, por exemplo, isso é a base de todos os managers de objetos — de NPC até efeitos.
5. Exercício prático: desenhando figuras diferentes
Bora ver um exemplo fora do zoológico, agora de gráficos. Vamos criar uma classe base Shape com um método virtual Draw(), e depois expandir ela.
public class Shape
{
public virtual void Draw()
{
Console.WriteLine("Desenhando uma figura indefinida.");
}
}
public class Circle : Shape
{
public override void Draw()
{
Console.WriteLine("Desenhando um círculo.");
}
}
public class Rectangle : Shape
{
public override void Draw()
{
Console.WriteLine("Desenhando um retângulo.");
}
}
// Coleção de figuras
Shape[] shapes = new Shape[]
{
new Circle(),
new Rectangle(),
new Circle()
};
foreach (var shape in shapes)
{
shape.Draw();
}
Aqui o Draw() é chamado na variável do tipo Shape, mas na real são chamados os métodos de Circle ou Rectangle. Em editores gráficos e libs de verdade, tipo WinForms ou WPF — é assim mesmo que funciona.
6. Jeito geral: escrevendo algoritmos universais
O polimorfismo transforma seu código numa ferramenta flexível e fácil de expandir. Por exemplo, na coleção podem ter não só Dog e Cat, mas também Hamster, Papagaio e o que mais você quiser — e você não escreve nem uma linha nova pra tratar todos eles do mesmo jeito.
Além disso, você pode passar objetos-filhos pra métodos que aceitam o tipo base numa boa:
void PrintAnimalInfo(Animal animal)
{
Console.WriteLine($"Nome: {animal.Name}");
animal.MakeSound();
animal.Move();
}
Animal hamster = new Animal("Homa");
Animal dog = new Dog("Lord");
PrintAnimalInfo(hamster); // Usa o método de Animal
PrintAnimalInfo(dog); // Usa a versão de Dog
7. Dicas e detalhes
Perguntas comuns e pegadinhas
Muita gente que tá começando acha que se criar um objeto Dog e depois declarar a variável Dog myDog, não tem diferença pra Animal myDog. Mas na real, se declarar como Animal, ela "enxerga" só o que tem em Animal (tirando os métodos sobrescritos), e se declarar como Dog — vê tudo: Bark() e propriedades específicas.
Outra coisa: se o método na classe base não for virtual, não dá pra sobrescrever. Se tentar botar override no filho pra esse método — o compilador vai reclamar e dar erro.
Aliás, se você realmente quiser substituir um método que não é virtual, usa a palavra new, mas isso já é papo pra casos avançados (e polêmicos!).
Por que isso cai tanto em entrevista?
Porque polimorfismo é tipo canivete suíço de programador: se você entende como usar, consegue criar sistemas fáceis de expandir e manter, seu código não só funciona, mas dura muito. Por exemplo, podem pedir pra você implementar o processamento de vários tipos de pagamento (clássico: BankCard, PayPal, Bitcoin) — e esperam que você faça uma interface comum (ou classe base abstrata) com o método Pay(), e aí os clientes podem chamar Pay(BankCard), Pay(PayPal), Pay(Bitcoin) — sem saber como funciona por dentro.
8. Erros comuns de quem tá começando
Um dos erros mais comuns é tentar chamar um método específico da classe derivada usando uma variável do tipo base. Tipo assim:
Animal animal = new Dog("Tuzik");
animal.Bark(); // Erro! Animal não tem método Bark.
Por que isso não rola? Porque a variável do tipo Animal só "vê" o que foi declarado em Animal, mesmo que na real seja um Dog. Só dá pra chamar o que foi definido na classe base — e sobrescrito (override) nos filhos.
Se você realmente precisar chamar um método que só existe em Dog — vai ter que fazer cast:
Animal animal = new Dog("Tuzik");
if (animal is Dog dog)
{
dog.Bark();
}
Mas geralmente, se você tá fazendo isso, é sinal de que tem algo errado na arquitetura (ou tá exagerando na herança).
GO TO FULL VERSION