C#: Polimorfismo
1. Visão geral do polimorfismo
O polimorfismo é uma das principais características da programação orientada a objetos, referindo-se à capacidade de uma única interface apresentar comportamentos diferentes em objetos distintos. Em C#, o polimorfismo se apresenta de duas formas:
- Polimorfismo em tempo de execução: Obtido por meio de
virtual/overridepara o despacho dinâmico; o método a ser chamado é determinado em tempo de execução - Polimorfismo em tempo de compilação: obtido por meio da sobrecarga de métodos, em que o compilador seleciona o destino da chamada com base na assinatura do parâmetro (abordado na Lição 14)
Esta aula aborda o polimorfismo em tempo de execução, que é a capacidade mais importante no projeto orientado a objetos.
2. virtual e override
A classe base marca um método como substituível com virtual, e a classe derivada fornece uma nova implementação com override. Em tempo de execução, o método a ser chamado depende do tipo real do objeto (e não do tipo declarado da variável) — isso é o despacho dinâmico.
▶ Exemplo
using System;
class Animal
{
public virtual void Speak()
{
Console.WriteLine("Animal makes a sound");
}
}
class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Dog barks");
}
}
class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Cat meows");
}
}
class Program
{
static void Main()
{
Animal a = new Dog();
a.Speak();
Animal b = new Cat();
b.Speak();
}
}
Dog barks
Cat meows
As variáveis são declaradas como Animal, mas os objetos reais são Dog e Cat. Durante a execução, cada versão sobrescrita é chamada — esse é o efeito central do polimorfismo.
3. Aplicação prática: interface unificada, diferentes implementações
O cenário mais comum para o polimorfismo é o uso de uma matriz ou coleção de referências à classe base para gerenciar diferentes objetos de classes derivadas, chamando uma interface unificada enquanto cada objeto responde à sua maneira.
▶ Exemplo
using System;
class Animal
{
public virtual void Speak()
{
Console.WriteLine("Animal makes a sound");
}
}
class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Dog barks");
}
}
class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Cat meows");
}
}
class Cow : Animal
{
public override void Speak()
{
Console.WriteLine("Cow moos");
}
}
class Program
{
static void MakeThemSpeak(Animal[] animals)
{
foreach (Animal a in animals)
{
a.Speak();
}
}
static void Main()
{
Animal[] animals = { new Dog(), new Cat(), new Cow() };
MakeThemSpeak(animals);
}
}
Dog barks
Cat meows
Cow moos
O método MakeThemSpeak depende apenas do tipo Animal. Ele não precisa saber com qual animal específico está lidando, e a adição de novas classes derivadas não requer nenhuma modificação nesse método.
4. override x new
O uso da palavra-chave new em uma classe derivada pode ocultar um método da classe base, mas isso não faz parte do polimorfismo. override participa do despacho dinâmico, enquanto a ocultação por new seleciona o método em tempo de compilação com base no tipo declarado.
▶ Exemplo
using System;
class Base
{
public virtual void Print()
{
Console.WriteLine("Base.Print");
}
}
class OverrideDerived : Base
{
public override void Print()
{
Console.WriteLine("OverrideDerived.Print");
}
}
class NewDerived : Base
{
public new void Print()
{
Console.WriteLine("NewDerived.Print");
}
}
class Program
{
static void Main()
{
Base obj1 = new OverrideDerived();
obj1.Print();
Base obj2 = new NewDerived();
obj2.Print();
NewDerived obj3 = new NewDerived();
obj3.Print();
}
}
OverrideDerived.Print
Base.Print
NewDerived.Print
obj1:overridesobrescrita — mesmo por meio de uma referência à classe base, a versão da classe derivada é chamadaobj2:newocultação — por meio de uma referência à classe base, é chamada a versão da classe base; o polimorfismo é prejudicadoobj3: Só é possível acessar a versão ocultanewpor meio de uma referência a uma classe derivada
override quando precisar de comportamento polimórfico. Use new apenas para ocultar métodos em casos especiais.
5. Métodos abstratos e classes abstratas
Um método abstract não possui implementação e obriga as classes derivadas a sobrescrevê-lo. Uma classe que contenha métodos abstratos deve ser declarada como abstract e não pode ser instanciada diretamente.
▶ Exemplo
using System;
abstract class Shape
{
public abstract double Area();
}
class Circle : Shape
{
public double Radius { get; set; }
public Circle(double radius)
{
Radius = radius;
}
public override double Area()
{
return Math.PI * Radius * Radius;
}
}
class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}
public override double Area()
{
return Width * Height;
}
}
class Program
{
static void PrintArea(Shape shape)
{
Console.WriteLine($"Area: {shape.Area():F2}");
}
static void Main()
{
Shape[] shapes = { new Circle(3), new Rectangle(4, 5) };
foreach (Shape s in shapes)
{
PrintArea(s);
}
}
}
Area: 28.27
Area: 20.00
A classe abstrata define “o que é”, enquanto as classes derivadas determinam “como funciona”. Todas as formas calculam sua área por meio da interface unificada Area().
6. Encadeamento de métodos: substituição em vários níveis
virtual/override podem ser sobrescritos camada por camada ao longo da cadeia de herança. Uma classe derivada pode chamar a implementação da classe pai por meio de base. e ampliá-la ainda mais.
▶ Exemplo
using System;
class Animal
{
public virtual void Speak()
{
Console.WriteLine("Animal speaks");
}
}
class Dog : Animal
{
public override void Speak()
{
base.Speak();
Console.WriteLine("Dog barks");
}
}
class Puppy : Dog
{
public override void Speak()
{
base.Speak();
Console.WriteLine("Puppy yips");
}
}
class Program
{
static void Main()
{
Animal a = new Puppy();
a.Speak();
}
}
Animal speaks
Dog barks
Puppy yips
Puppy.Speak() chama base.Speak(), o que aciona Dog.Speak(), que, por sua vez, chama base.Speak(), acionando Animal.Speak(), formando uma cadeia de saída da classe base para a classe derivada.
7. Sobrescrevendo o método ToString
Todos os tipos em C# herdam de object, e seu método ToString() é virtual. A redefinição desse método permite que um objeto exiba um texto significativo ao ser exibido.
▶ Exemplo
using System;
class Student
{
public string Name { get; set; }
public int Age { get; set; }
public Student(string name, int age)
{
Name = name;
Age = age;
}
public override string ToString()
{
return $"{Name} (Age: {Age})";
}
}
class Program
{
static void Main()
{
Student s = new Student("Alice", 20);
Console.WriteLine(s);
Console.WriteLine(s.ToString());
}
}
Alice (Age: 20)
Alice (Age: 20)
Console.WriteLine chama automaticamente ToString() internamente. Após a substituição, a saída passa do nome do tipo padrão para um formato personalizado.
8. O Princípio de Substituição de Liskov
O Princípio de Substituição de Liskov (LSP) é um princípio fundamental do projeto orientado a objetos: os objetos da classe derivada devem poder substituir os objetos da classe base sem alterar a correção do programa.
▶ Exemplo
using System;
class Bird
{
public virtual void Fly()
{
Console.WriteLine("Bird flies");
}
}
class Swallow : Bird
{
public override void Fly()
{
Console.WriteLine("Swallow flies high");
}
}
class Penguin : Bird
{
public override void Fly()
{
throw new NotSupportedException("Penguin cannot fly");
}
}
class Program
{
static void LetFly(Bird bird)
{
bird.Fly();
}
static void Main()
{
LetFly(new Swallow());
LetFly(new Penguin());
}
}
Swallow flies high
Unhandled Exception: System.NotSupportedException: Penguin cannot fly
Penguin herda de Bird, mas sobrescreve Fly para lançar uma exceção, quebrando o contrato “pássaros podem voar” da classe base e violando o LSP. A abordagem correta é redesenhar a hierarquia de herança, por exemplo, extraindo a “capacidade de voar” para uma interface separada.
❓ Perguntas Frequentes
P: É possível deixar um método
virtualsem ser sobrescrito? R: Sim. Se não for sobrescrito, será utilizada a implementação padrão da classe base.
P: Um método
overridepode ser sobrescrito novamente? R: Sim. Um métodooverridecarrega implicitamente a semânticavirtual, portanto, classes derivadas posteriores podem continuar a sobrescrevê-lo.
P: O que é melhor,
newouoverride? R: Useoverridequando precisar de comportamento polimórfico. Usenewsomente quando for necessário ocultar um método e tiver certeza de que o polimorfismo não é necessário.
P: Qual é a diferença entre uma classe abstrata e uma interface? R: Uma classe abstrata pode ter campos e código de implementação; uma interface define apenas um contrato. Uma classe pode herdar de apenas uma classe abstrata, mas pode implementar várias interfaces.
P: O que acontece se eu não sobrescrever
ToString()? R: O padrão retorna o nome completo do tipo; por exemplo,Studentresultaria emStudent.
📖 Resumo
- O polimorfismo se apresenta em duas formas: em tempo de execução (
virtual/override) e em tempo de compilação (sobrecarregamento) virtualindica um método que pode ser sobrescrito;overridefornece uma nova implementação na classe derivadaabstractesses métodos obrigam as classes derivadas a sobrescrever; as classes abstratas não podem ser instanciadasoverrideparticipa do despacho dinâmico;newoculta o método da classe base e quebra o polimorfismo- Chamar diferentes métodos de classes derivadas por meio de uma referência à classe base é a aplicação típica do polimorfismo
- O Princípio de Substituição de Liskov exige que as classes derivadas possam substituir totalmente suas classes base
- A sobrescrita de
ToString()é uma das aplicações mais comuns do polimorfismo na programação do dia a dia
📝 Exercícios
- Defina uma classe base
Vehiclecom um métodovirtual void Move(), crie as classes derivadasCareBicycleque sobrescrevemMove(), e chame-as uniformemente por meio de uma matrizVehicle[]emMain - Defina uma classe abstrata
Paymentcom um métodoabstract void Pay(decimal amount)e crie as classes derivadasCashPaymenteCardPayment, que implementem sua própria lógica de pagamento - Crie uma classe
Booke sobrescrevaToString()para exibir o título e o preço do livro; verifique o resultado deConsole.WriteLine(book) - Escreva um código que demonstre a diferença entre
overrideenew: use uma referência à classe base para chamar, respectivamente, dois objetos de classes derivadas e observe a diferença nos resultados - Considere o seguinte: se
Squareherda deRectangle, mas substituiSetWidth/SetHeightde modo que os comprimentos dos lados sejam sempre iguais, isso viola o Princípio de Substituição de Liskov? Explique seu raciocínio.