1. Herança — especializando uma classe
Herança permite que uma classe (derivada) reaproveite membros de outra (base), adicionando ou substituindo comportamento:
public class Funcionario
{
public string Nome { get; set; } = string.Empty;
protected decimal SalarioBase { get; set; }
public virtual decimal CalcularSalario() => SalarioBase;
}
public class Gerente : Funcionario
{
public decimal Bonus { get; set; }
public override decimal CalcularSalario() => base.CalcularSalario() + Bonus;
}
var gerente = new Gerente { Nome = "Ana", SalarioBase = 5000, Bonus = 1000 };
// Erro: SalarioBase é 'protected' e Nome/Bonus foram atribuídos via inicializador de objeto
// (válido pois são 'public' e Gerente herda de Funcionario)
Console.WriteLine(gerente.CalcularSalario()); // 6000
| Palavra-chave | Papel |
|---|---|
: Funcionario | Declara que Gerente herda de Funcionario |
virtual | Marca um método da classe base como "pode ser substituído" |
override | Substitui a implementação de um método virtual da base |
base. | Chama a implementação original (da classe base) de dentro da versão sobrescrita |
protected | Visível na própria classe e em qualquer classe que herde dela |
Sem virtual na classe base, override na
derivada não compila. Esquecer virtual é o motivo mais comum de "meu
método sobrescrito não está sendo chamado" para quem começa em C#.
2. Classes abstratas
Uma classe abstract não pode ser instanciada diretamente — ela existe
só para ser herdada, geralmente definindo uma estrutura que as subclasses são obrigadas a
completar:
public abstract class FormaGeometrica
{
public abstract double CalcularArea(); // sem corpo — obrigatório na subclasse
public void Descrever() // método concreto, herdado como está
=> Console.WriteLine($"Área: {CalcularArea():F2}");
}
public class Retangulo : FormaGeometrica
{
public double Largura { get; set; }
public double Altura { get; set; }
public override double CalcularArea() => Largura * Altura;
}
// var forma = new FormaGeometrica(); -- não compila
var retangulo = new Retangulo { Largura = 4, Altura = 5 };
retangulo.Descrever(); // "Área: 20.00"
3. Interfaces — contrato sem implementação
Uma interface declara o quê uma classe deve fazer, sem dizer como. Diferente de herança (uma classe só pode ter uma classe base), uma classe pode implementar várias interfaces:
public interface INotificavel
{
void Notificar(string mensagem);
}
public interface IAuditavel
{
DateTime CriadoEm { get; }
}
public class Cliente : INotificavel, IAuditavel
{
public DateTime CriadoEm { get; } = DateTime.Now;
public void Notificar(string mensagem)
=> Console.WriteLine($"Enviando: {mensagem}");
}
| Classe abstrata | Interface | |
|---|---|---|
| Uma classe pode ter quantas? | Só uma (herança única) | Quantas quiser |
| Pode ter implementação padrão? | Sim, métodos concretos + abstratos | Sim, desde C# 8 (default interface methods) — mas raro na prática |
| Pode ter campos/estado? | Sim | Não |
| Uso típico | "É um" — relação de especialização (Gerente é um Funcionario) | "Pode fazer" — capacidade (Cliente pode ser notificado) |
4. Polimorfismo — tratando tipos diferentes de forma uniforme
Este é o benefício prático de tudo o que veio antes: uma variável (ou parâmetro, ou item de lista) do tipo base pode guardar um objeto de qualquer classe derivada, e o método correto é chamado automaticamente:
List<FormaGeometrica> formas = new()
{
new Retangulo { Largura = 4, Altura = 5 },
new Circulo { Raio = 3 },
new Triangulo { Base = 6, Altura = 4 },
};
foreach (var forma in formas)
forma.Descrever(); // chama a versão correta de CalcularArea() para cada tipo
Fig. 1 — Polimorfismo: código escrito contra a classe base funciona com qualquer subclasse.
Se você já usou TypeScript no Node, isso deve parecer familiar — interface
existe com o mesmo espírito lá. A diferença é que em TypeScript a checagem de tipos desaparece na
compilação para JS puro (é só uma ferramenta de desenvolvimento); em C#, o sistema de tipos é
real e existe também em tempo de execução — dá para checar o tipo real de um objeto com
is ou GetType() mesmo depois de compilado.
5. sealed — impedindo herança futura
public sealed class RelatorioFinal
{
// ninguém pode herdar desta classe
}
// public class RelatorioEspecial : RelatorioFinal { } -- não compila
Use sealed quando uma classe representa um conceito final e completo
— evita que futuras "especializações" acidentais quebrem premissas que o design original assumia.
6. Onde isso te leva
Com herança, interfaces e polimorfismo cobertos, você tem a base de orientação a objetos completa.
O próximo capítulo aplica tudo isso a coleções — List<T>,
Dictionary<TKey, TValue> e generics, que você já usou de forma
intuitiva nos exemplos até aqui.