1. Introdução
Bora voltar pro nosso zoológico. A gente tem uma classe base Animal (Animal) e suas derivadas: Dog (Cachorro), Cat (Gato), Fish (Peixe).
Nas aulas anteriores, a gente colocou na Animal um método virtual MakeSound():
public class Animal
{
public string Name { get; set; }
public int Age { get; set; }
public virtual void MakeSound() // Método virtual
{
Console.WriteLine("Algum animal faz um som."); // Implementação padrão
}
public void Sleep() // Método normal
{
Console.WriteLine($"{Name} dorme.");
}
}
public class Dog : Animal
{
public override void MakeSound() // Sobrescrevendo o som do cachorro
{
Console.WriteLine("Au-au!");
}
}
public class Cat : Animal
{
public override void MakeSound() // Sobrescrevendo o som do gato
{
Console.WriteLine("Miau!");
}
}
Isso funciona muito bem! Quando a gente cria um Dog ou Cat e chama MakeSound(), ouvimos o som único deles. Mas e se a gente criar só um Animal?
Animal genericAnimal = new Animal();
genericAnimal.Name = "Criatura desconhecida";
genericAnimal.MakeSound(); // Vai mostrar: "Algum animal faz um som."
Parece lógico. Mas às vezes, a implementação padrão simplesmente não faz sentido. E se Animal não for um animal específico, mas sim uma ideia? "Animal" em si não faz um som específico, quem faz são os tipos concretos. Ou imagina que temos uma classe Shape (Forma) e nela um método CalculateArea() (CalcularÁrea). Que área uma simples Forma deveria calcular? Nenhuma! Área tem o Círculo, o Quadrado, mas não a "Forma" abstrata.
Nesses casos, quando a classe base não pode (ou não deve) dar uma implementação padrão com sentido, mas obriga todas as classes derivadas a implementar esse método, entram em cena os métodos abstratos e as classes abstratas.
2. Classes abstratas: quando o projeto ainda não é a casa
Imagina que você é arquiteto e tá desenhando o projeto de uma casa padrão. Mas não é qualquer casa, é uma "Casa Conceitual". Ela tem coisas em comum: paredes, telhado, fundação. Mas você ainda não sabe se vai ser um sobrado ou um arranha-céu. Algumas partes do projeto são concretas (tipo a altura do teto do primeiro andar), outras são só uma ideia (tipo "quantidade de andares", que vai ser definida depois).
No mundo do C#, essa "Casa Conceitual" é chamada de classe abstrata.
Classe abstrata é uma classe marcada com a palavra-chave abstract.
public abstract class Animal // Agora Animal é uma classe abstrata
{
// ...
}
Ponto chave das classes abstratas:
- Não dá pra criar uma instância delas direto. Você não pode escrever new Animal(). Por quê? Porque Animal agora é algo indefinido, uma ideia. Você não constrói uma "Casa Conceitual", só um sobrado ou um arranha-céu concreto.
Se tentar new Animal(), o compilador já te dá um puxão de orelha:
Isso é uma limitação importante!Cannot create an instance of the abstract type or interface 'Animal' (Não é possível criar uma instância do tipo abstrato ou interface 'Animal') - Elas podem ter membros abstratos. E é aí que fica interessante!
3. Métodos abstratos: contrato sem implementação
Se a classe abstrata é a "Casa Conceitual", o método abstrato é aquela parte do projeto marcada como "fazer isso", mas sem instrução de "como". Tipo, "Construir a fundação" — é obrigatório na casa, mas o tamanho e o material dependem do tipo da casa.
Método abstrato é um método que:
- É marcado com a palavra-chave abstract.
- Não tem corpo (ou seja, não tem bloco de código {}). Ele termina com ponto e vírgula ;.
- Só pode ser declarado dentro de uma classe abstrata.
Bora deixar nosso método MakeSound() abstrato:
public abstract class Animal // Agora Animal é abstrata
{
public string Name { get; set; }
public int Age { get; set; }
public abstract void MakeSound(); // Olha aí, método abstrato! Sem corpo!
public void Sleep() // Esse método continua normal, "concreto"
{
Console.WriteLine($"{Name} dorme.");
}
}
Olha como mudou o MakeSound()! Não tem mais chaves nem implementação padrão. Agora ele só diz: "Todo animal tem que saber fazer um som. Como? Quem herdar de mim que decida."
Regra importante: Se sua classe herda de uma classe abstrata e não é abstrata também, ela tem que sobrescrever (com override) todos os métodos abstratos da base. Não é opcional, é obrigação, contrato! O compilador C# é bem rígido nisso. Se esquecer, ele já te avisa:
public class Dog : Animal // Classe normal, não abstrata
{
// ERRO DE COMPILAÇÃO!
// 'Dog' does not implement inherited abstract member 'Animal.MakeSound()'
// (Classe 'Dog' não implementa o membro abstrato herdado 'Animal.MakeSound()')
// O compilador espera que a gente faça o MakeSound() com override!
}
Pra resolver o erro, Dog e Cat têm que sobrescrever MakeSound():
public class Dog : Animal
{
public override void MakeSound() // Tem que sobrescrever!
{
Console.WriteLine("Au-au!");
}
}
public class Cat : Animal
{
public override void MakeSound() // Aqui também!
{
Console.WriteLine("Miau!");
}
}
Comparando virtual, abstract e override
| Característica | virtual método | abstract método | override palavra-chave |
|---|---|---|---|
| Onde pode estar | Em classe normal ou abstrata | Só em classe abstrata | Na classe derivada (filha) |
| Corpo do método | Tem corpo (implementação padrão) | Não tem corpo (termina com ;) | Tem corpo (nova implementação) |
| Objetivo | Fornece implementação padrão, mas deixa as filhas mudarem se quiserem | Declara um contrato: as filhas têm que implementar | Fornece implementação específica pra método virtual ou abstract da base |
| Pode criar instância da base? | Sim (se a base não for abstrata) | Não (se a base for abstrata) | N/A (vale pro método, não pra classe) |
| Obrigação da classe filha | Opcional sobrescrever (override) | Obrigatório sobrescrever (override), se a filha não for abstrata | N/A |
4. Polimorfismo na prática com métodos abstratos
Agora começa a parte mais legal! Já vimos que polimorfismo deixa a gente trabalhar com objetos de várias classes filhas usando uma referência da classe base. E isso funciona até quando a base é abstrata!
Mesmo sem poder criar um Animal direto (new Animal() vai dar erro), a gente pode usar o tipo Animal como referência pra objetos das classes filhas. Isso é muito poderoso!
Bora continuar nosso zoológico. Imagina que temos uma fazenda, e nela moram vários animais. Queremos que cada animal faça seu som.
using System;
// Classe abstrata Animal
public abstract class Animal
{
public string Name;
public int Age;
public Animal(string name, int age) { Name = name; Age = age; }
public abstract void MakeSound();
public void Sleep() { Console.WriteLine($"{Name} dorme."); }
}
public class Dog : Animal
{
public Dog(string name, int age) : base(name, age) { }
public override void MakeSound() { Console.WriteLine("Au-au!"); }
}
public class Cat : Animal
{
public Cat(string name, int age) : base(name, age) { }
public override void MakeSound() { Console.WriteLine("Miau!"); }
}
public class Fish : Animal
{
public Fish(string name, int age) : base(name, age) { }
public override void MakeSound() { Console.WriteLine("Blub-blub"); }
}
class Program
{
static void Main()
{
Animal[] animals = {
new Dog("Sharik", 3),
new Cat("Murzik", 5),
new Fish("Nemo", 1)
};
foreach (Animal animal in animals)
{
Console.WriteLine($"\nOi, eu sou {animal.Name}, tenho {animal.Age} anos.");
animal.MakeSound();
animal.Sleep();
}
}
}
O que rola nesse código?
- A gente declarou Animal como abstract class. Isso diz pro compilador: "Essa classe é um molde, não pode ser criada, mas pode ser herdada".
- A gente declarou public abstract void MakeSound(); dentro de Animal. Isso fala: "Toda classe que herda de Animal (e não é abstrata) tem que implementar o método MakeSound()". É nosso contrato!
- Dog, Cat e Fish cumprem esse contrato, sobrescrevendo MakeSound() com suas implementações. Se a gente esquecesse em alguma delas, o compilador não deixava passar.
- No método Main a gente cria um array Animal[]. Mesmo Animal sendo abstrata, o array pode guardar referências pra objetos das filhas (Dog, Cat, Fish), porque elas são Animal!
- Quando a gente passa pelo array no foreach e chama animal.MakeSound(), graças ao polimorfismo o C# "sabe" qual MakeSound() chamar: Dog.MakeSound(), Cat.MakeSound() ou Fish.MakeSound(). Ele chama o método do tipo real do objeto, não do tipo da referência. Essa é a mágica do polimorfismo!
- Já o animal.Sleep() chama a implementação concreta da base Animal, porque esse método não foi marcado como virtual ou abstract, nem foi sobrescrito nas filhas.
5. Pra que serve isso na vida real?
"Beleza, zoológico, animais... Mas onde isso vai servir quando eu for fazer um app de banco ou loja?" — você pode perguntar. E é uma ótima pergunta! Classes e métodos abstratos são uma baita ferramenta pra criar sistemas flexíveis e fáceis de expandir.
Forçar a implementação do contrato: Esse é o maior benefício. Imagina que você tá criando um framework pra sistemas de pagamento. Você tem uma base abstract class PaymentProcessor (ProcessadorDePagamento). E você sabe que todo processador de pagamento tem que saber ProcessPayment() (ProcessarPagamento), RefundPayment() (EstornarPagamento) e CheckStatus() (ChecarStatus). Mas como faz isso no PayPal, cartão ou Bitcoin — é totalmente diferente.
Você declara esses métodos como abstract em PaymentProcessor.
public abstract class PaymentProcessor
{
public abstract bool ProcessPayment(decimal amount, string currency, string cardNumber);
public abstract bool RefundPayment(string transactionId);
public abstract string CheckStatus(string transactionId);
// ... outros métodos que podem ser concretos, tipo log
public void LogTransaction(string message)
{
Console.WriteLine($"[LOG]: {message}");
}
}
public class PayPalProcessor : PaymentProcessor
{
public override bool ProcessPayment(decimal amount, string currency, string cardNumber)
{
// Aqui vai a lógica do API do PayPal
Console.WriteLine($"PayPal: processando {amount} {currency}...");
return true;
}
public override bool RefundPayment(string transactionId) { /* ... */ return true; }
public override string CheckStatus(string transactionId) { /* ... */ return "Completed"; }
}
public class CreditCardProcessor : PaymentProcessor
{
public override bool ProcessPayment(decimal amount, string currency, string cardNumber)
{
// Aqui vai a lógica do banco adquirente
Console.WriteLine($"CreditCard: {amount} {currency} do cartão {cardNumber.Substring(0,4)}XXXX...");
return true;
}
public override bool RefundPayment(string transactionId) { /* ... */ return true; }
public override string CheckStatus(string transactionId) { /* ... */ return "Processing"; }
}
Agora, qualquer dev que quiser criar um novo processador de pagamento (tipo BitcoinProcessor) vai ser obrigado a implementar esses três métodos. Não tem como esquecer o RefundPayment(), porque o compilador não deixa! Isso garante consistência no seu sistema.
Flexibilidade e expansão: Você pode escrever código que trabalha com PaymentProcessor sem saber qual implementação vai ser usada. Por exemplo, no carrinho da loja, você só chama currentProcessor.ProcessPayment(), e o sistema escolhe o processador certo conforme o método de pagamento do usuário. Amanhã aparece um novo método — só criar uma classe nova, herdar de PaymentProcessor, implementar os métodos abstratos, e o código principal da loja nem precisa mudar!
Evitar implementações vazias: Se a gente usasse métodos virtual em vez de abstract, teria que dar uma implementação vazia padrão, o que pode ser confuso. abstract deixa claro: "Aqui não tem implementação, e nem pode ter — vai pro filho!"
Melhora a arquitetura do código: Classes abstratas ajudam a separar bem o que é lógica geral e o que é específico. O geral (tipo LogTransaction em PaymentProcessor) fica na base, o específico (tipo ProcessPayment) nas filhas. Isso deixa o código mais legível, fácil de manter e de testar. Permite que quem faz frameworks e libs defina "pontos de extensão" que os usuários têm que preencher.
6. Erros comuns e pegadinhas
Erro #1: tentar criar instância de classe abstrata.
Sempre dá erro de compilação. Classe abstrata é conceito, não objeto concreto. Você pode usar como tipo de referência, mas não pode escrever new Animal().
Erro #2: esqueceu de sobrescrever método abstrato.
Se a classe filha não for abstrata, ela tem que implementar todos os métodos abstract da base. Senão — erro de compilação. Só dá pra escapar disso tornando a filha abstrata também, mas quase nunca é o que você quer.
Erro #3: confundir abstract com virtual.
abstract obriga a sobrescrever, não tem corpo. virtual dá implementação padrão e pode ser sobrescrito. Não pode declarar método abstract com corpo nem virtual sem corpo — isso é erro de sintaxe.
Erro #4: usar new em vez de override.
Se você não usa override e só cria um método com o mesmo nome na filha, você não sobrescreve, só esconde o método da base. Isso pode dar comportamento estranho em chamadas polimórficas: vai chamar o método da base.
public class Base
{
public void DoSomething() { Console.WriteLine("Base"); }
}
public class Derived : Base
{
public new void DoSomething() { Console.WriteLine("Derived"); }
}
Base obj = new Derived();
obj.DoSomething(); // Vai mostrar: Base
GO TO FULL VERSION