Introdução
Imagina que você tá fazendo um jogo com vários tipos de monstro, mas ainda sem polimorfismo. Cada monstro é só um "tipo" e uma função central decide o que cada um faz:
void Atacar(string tipoMonstro)
{
if (tipoMonstro == "Dragao")
Console.WriteLine("🔥 Cospe uma bola de fogo!");
else if (tipoMonstro == "Fantasma")
Console.WriteLine("👻 Atravessa a parede e assusta!");
else if (tipoMonstro == "Slime")
Console.WriteLine("🟢 Pula e gruda em você!");
// toda vez que você inventa um monstro novo, tem que vir AQUI e adicionar mais um "else if"
}
Isso funciona, mas é uma bomba-relógio: essa função vai crescer pra sempre, e todo mundo que mexe no jogo precisa saber que ela existe e lembrar de atualizá-la. É frágil e cansativo.
A ideia central do polimorfismo
Em vez de uma função central que pergunta "que tipo é você?", cada monstro já sabe se comportar sozinho. Você não pergunta, você só manda:
**abstract** class Monstro
{
public string Nome { get; }
public int Vida { get; protected set; } = 100;
protected Monstro(string nome) => Nome = nome;
public abstract void Atacar();
public virtual void TomarDano(int dano)
{
Vida -= dano;
Console.WriteLine($"{Nome} perdeu {dano} de vida (restam {Vida}).");
}
}
E cada monstro implementa sua própria versão:
class Dragao : Monstro
{
public Dragao() : base("Dragão") { }
public override void Atacar() => Console.WriteLine($"{Nome} 🔥 cospe uma bola de fogo!");
}
class Fantasma : Monstro
{
public Fantasma() : base("Fantasma") { }
public override void Atacar() => Console.WriteLine($"{Nome} 👻 atravessa a parede e assusta!");
public override void TomarDano(int dano)
{
int danoReal = dano / 2; // é intangível, sofre metade do dano
Console.WriteLine($"{Nome} é intangível — dano reduzido pela metade!");
base.TomarDano(danoReal);
}
}
class Slime : Monstro
{
public Slime() : base("Slime") { }
public override void Atacar() => Console.WriteLine($"{Nome} 🟢 pula e gruda em você!");
}
class Golem : Monstro
{
public Golem() : base("Golem") { Vida = 200; } // é um tanque, vida maior
public override void Atacar() => Console.WriteLine($"{Nome} 🪨 esmaga o chão com o punho!");
}
Repara que Fantasma não só ataca diferente — ele reage a dano diferente também, porque sobrescreveu TomarDano. Cada classe carrega sua própria lógica, não só um texto diferente.
Colocando pra rodar: a batalha completa
List<Monstro> inimigos = new() { new Dragao(), new Fantasma(), new Slime(), new Golem() };
foreach (var inimigo in inimigos)
{
inimigo.Atacar();
inimigo.TomarDano(30);
Console.WriteLine();
}
Saída:
Dragão 🔥 cospe uma bola de fogo!
Dragão perdeu 30 de vida (restam 70).
Fantasma 👻 atravessa a parede e assusta!
Fantasma é intangível — dano reduzido pela metade!
Fantasma perdeu 15 de vida (restam 85).
Slime 🟢 pula e gruda em você!
Slime perdeu 30 de vida (restam 70).
Golem 🪨 esmaga o chão com o punho!
Golem perdeu 30 de vida (restam 170).
Mesmo código (inimigo.Atacar(), inimigo.TomarDano(30)) rodando pra quatro tipos diferentes, cada um reagindo do seu próprio jeito. Zero if, zero switch.
O verdadeiro poder: extensibilidade
Quer adicionar um Vampiro que rouba vida ao atacar? Você cria a classe...
class Vampiro : Monstro
{
public Vampiro() : base("Vampiro") { }
public override void Atacar() => Console.WriteLine($"{Nome} 🧛 morde e rouba sua vida!");
}
...e pronto. Não precisa tocar em nenhuma linha da lista, do foreach, ou de qualquer código que já existia. Isso é o Princípio Aberto/Fechado (aquele "O" do SOLID) na prática: o sistema está aberto pra crescer, mas fechado pra você precisar reabrir e editar o que já funciona.
Os dois tipos de polimorfismo (pra fechar com chave de ouro)
Tudo que fizemos até aqui é polimorfismo em tempo de execução (via override) — o mais importante e o que todo mundo quer dizer quando fala "polimorfismo" em POO. Mas existe um segundo tipo, mais simples, decidido em tempo de compilação: sobrecarga de métodos.
class Curandeiro
{
public void Curar(Monstro alvo) => alvo.TomarDano(-20); // cura um
public void Curar(List<Monstro> aliados)
{
foreach (var a in aliados) a.TomarDano(-20); // cura vários
}
}
Aqui o compilador escolhe qual Curar chamar baseado nos argumentos que você passa — antes mesmo do programa rodar. Não tem nada de dinâmico, é só "mesmo nome, assinaturas diferentes".
Como o C# sabe qual versão rodar
A parte que parece mágica: repare que a lista é List<Monstro>, não List<Dragao> ou qualquer coisa específica.
Monstro m = new Fantasma();
m.Atacar(); // roda a versão do Fantasma, mesmo "m" sendo do tipo Monstro
O C# decide qual Atacar() rodar olhando pro tipo real do objeto guardado na memória — não pro tipo da variável que você usou pra referenciar ele. É por isso que o foreach funciona perfeitamente numa lista cheia de tipos misturados: em cada volta, o objeto "lembra" quem ele realmente é.
Top comments (0)