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:

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

CSHARP
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();
    }
}
▶ Experimente
TEXT 📖 Somente leitura
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

CSHARP 📖 Somente leitura
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);
    }
}
44 linhas de lógica (limite de 40, somente leitura)
TEXT 📖 Somente leitura
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

CSHARP
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();
    }
}
▶ Experimente
TEXT 📖 Somente leitura
OverrideDerived.Print
Base.Print
NewDerived.Print
💡 Sempre use 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

CSHARP 📖 Somente leitura
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);
        }
    }
}
46 linhas de lógica (limite de 40, somente leitura)
TEXT 📖 Somente leitura
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

CSHARP
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();
    }
}
▶ Experimente
TEXT 📖 Somente leitura
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

CSHARP
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());
    }
}
▶ Experimente
TEXT 📖 Somente leitura
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.

⚠️ Violação típica do LSP: uma classe derivada sobrescreve um método e lança uma exceção não declarada na classe base, ou retorna um resultado que viola o contrato da classe base.

▶ Exemplo

CSHARP
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());
    }
}
▶ Experimente
TEXT 📖 Somente leitura
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 virtual sem ser sobrescrito? R: Sim. Se não for sobrescrito, será utilizada a implementação padrão da classe base.

P: Um método override pode ser sobrescrito novamente? R: Sim. Um método override carrega implicitamente a semântica virtual, portanto, classes derivadas posteriores podem continuar a sobrescrevê-lo.

P: O que é melhor, new ou override? R: Use override quando precisar de comportamento polimórfico. Use new somente 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, Student resultaria em Student.

📖 Resumo

📝 Exercícios

  1. Defina uma classe base Vehicle com um método virtual void Move(), crie as classes derivadas Car e Bicycle que sobrescrevem Move(), e chame-as uniformemente por meio de uma matriz Vehicle[] em Main
  2. Defina uma classe abstrata Payment com um método abstract void Pay(decimal amount) e crie as classes derivadas CashPayment e CardPayment, que implementem sua própria lógica de pagamento
  3. Crie uma classe Book e sobrescreva ToString() para exibir o título e o preço do livro; verifique o resultado de Console.WriteLine(book)
  4. Escreva um código que demonstre a diferença entre override e new: use uma referência à classe base para chamar, respectivamente, dois objetos de classes derivadas e observe a diferença nos resultados
  5. Considere o seguinte: se Square herda de Rectangle, mas substitui SetWidth/SetHeight de modo que os comprimentos dos lados sejam sempre iguais, isso viola o Princípio de Substituição de Liskov? Explique seu raciocínio.
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%