CodeGym /Cursos /C# SELF /Diferença entre interfaces e classes abstratas

Diferença entre interfaces e classes abstratas

C# SELF
Nível 23 , Lição 1
Disponível

1. Dois lados da mesma moeda

A gente já mergulhou bem fundo no conceito de abstração e viu dois dos instrumentos mais poderosos em C# – classes abstratas e interfaces. Você já deve ter percebido que eles são parecidos, porque ambos deixam a gente definir o "esqueleto" do comportamento que as classes concretas depois vão implementar. Mas, confia, é tipo comparar martelo e chave de fenda: são ferramentas, mas pra coisas diferentes.

Vamos começar pelo principal: por que a gente tem duas ferramentas pra uma coisa só? Em programação, igual na vida, nada é "só porque sim". Se existem duas ferramentas parecidas, é porque cada uma tem seus pontos fortes e usos próprios.

Bora ver logo uma tabela comparativa pra você pegar o geral. É tipo um resumão pra sacar os pontos mais importantes.

Característica Classe abstrata Interface
Criar instância Não dá pra criar direto (
new AbstractClass()
).
Não dá pra criar direto (
new IMyInterface()
).
Implementação dos membros Pode ter:
– Métodos/propriedades totalmente implementados.
– Métodos/propriedades abstratos (sem implementação).
– Campos, construtores, métodos estáticos.
Pode ter métodos com implementação padrão, membros estáticos abstratos e não abstratos.
Não pode ter campos de instância nem construtores.
Modificadores de acesso Pode ter qualquer um (public, protected, internal, private). Todos os membros da interface são public por padrão. Não precisa colocar modificador de acesso (exceção: métodos com implementação padrão e membros estáticos).
Herança Uma classe só pode herdar de uma classe abstrata. Uma classe pode implementar várias interfaces.
Tipo de relação "É um" (is-a). Define o tipo base e a parte comum da hierarquia. "Pode fazer" (has-a ou can-do). Define o contrato de comportamento/capacidade.
Estado Pode guardar estado (campos de instância). Pode ter campos estáticos, mas não campos de instância.
Extensão Dá pra adicionar novos métodos implementados sem mexer nos filhos. Dá pra adicionar métodos com implementação padrão sem quebrar os filhos.

E aí, curtiu? Bora destrinchar esses pontos.

2. Herança múltipla

Esse é, talvez, o ponto mais fundamental e fácil de lembrar. Em C#, igual em várias outras linguagens (tipo Java), uma classe só pode herdar de uma classe mãe. Pode ser classe normal ou abstrata – tanto faz, é uma só! Isso é pra evitar o famoso "problema do diamante" (Diamond Problem), quando herdar de várias classes pode dar confusão sobre qual método usar se eles tiverem o mesmo nome.

Agora, interfaces você pode implementar quantas quiser! Imagina que sua classe é uma pessoa. Ela pode ser "estudante" (herda da classe Student), mas também "sabe cozinhar" (ICookable), "sabe dirigir" (IDriveable) e "sabe cantar" (ISingable). Bem flexível, né?


abstract class Animal
{
    public string Name;
    public abstract void MakeSound();
    public void Eat() => Console.WriteLine($"{Name} come.");
}

interface IFlyable { void Fly(); double MaxFlyingAltitude { get; } }
interface ISwimable { void Swim(); }

class Duck : Animal, IFlyable, ISwimable
{
    public double MaxFlyingAltitude => 1000;
    public override void MakeSound() => Console.WriteLine($"{Name} faz quack!");
    public void Fly() => Console.WriteLine($"{Name} está voando!");
    public void Swim() => Console.WriteLine($"{Name} está nadando!");
}

class Program
{
    static void Main()
    {
        var duck = new Duck { Name = "Donald" };
        duck.MakeSound();
        duck.Eat();
        duck.Fly();
        duck.Swim();

        // Trabalhando com o objeto por tipos diferentes:
        Animal a = duck;  a.Eat();
        IFlyable f = duck; f.Fly();
        ISwimable s = duck; s.Swim();
    }
}

Se sua classe precisa ser parte de uma hierarquia (tipo, Dog é um Animal), usa herança de classe (abstrata ou normal). Se a classe precisa ter uma habilidade (tipo, Dog pode correr, Cat pode correr), mas essas habilidades não estão numa hierarquia só, usa interface. Esse é o maior trunfo das interfaces – criar contratos pra classes bem diferentes entre si.

3. Onde moram os dados e onde só tem promessa?

Classes abstratas podem ter de tudo:

  • Métodos e propriedades normais (não abstratos) com implementação completa.
  • Métodos e propriedades abstratos sem implementação (esses que a gente sobrescreve).
  • Campos (variáveis de instância) que guardam o estado do objeto.
  • Construtores pra inicializar esse estado.
  • Até métodos e propriedades estáticas.
  • E claro, podem ter qualquer modificador de acesso: public, protected, private etc.

Isso faz da classe abstrata uma ferramenta poderosa pra definir comportamento parcialmente implementado e estado comum pra todos os filhos.


abstract class Employee
{
    public string FirstName, LastName;
    public decimal Salary { get; protected set; }
    public Employee(string first, string last) { FirstName = first; LastName = last; }
    public void GetPaid(decimal sum)
    {
        Salary += sum;
        Console.WriteLine($"{FirstName} {LastName} recebeu {sum:C}. Salário: {Salary:C}");
    }
    public abstract void PerformWork();
    public abstract void TakeBreak();
}

class Developer : Employee
{
    public Developer(string f, string l) : base(f, l) { }
    public override void PerformWork() => Console.WriteLine($"{FirstName} está codando.");
    public override void TakeBreak() => Console.WriteLine($"{FirstName} está tomando café.");
}

class Tester : Employee
{
    public Tester(string f, string l) : base(f, l) { }
    public override void PerformWork() => Console.WriteLine($"{FirstName} está caçando bugs.");
    public override void TakeBreak() => Console.WriteLine($"{FirstName} está jogando futebol.");
}

class Program
{
    static void Main()
    {
        Employee[] team = {
            new Developer("Ivan", "Petrov"),
            new Tester("Maria", "Sidorova")
        };

        foreach (var emp in team)
        {
            Console.WriteLine($"\n--- Dia de trabalho para {emp.FirstName} {emp.LastName} ---");
            emp.PerformWork();
            emp.GetPaid(2000);
            emp.TakeBreak();
        }
    }
}

Interfaces – aí é outra pegada. Até o C# 8 elas só podiam ter declaração de métodos, propriedades, indexadores e eventos. Nada de campos, nada de construtores, nada de implementação! Só a "assinatura" do método, sem corpo. E todos os membros eram públicos por padrão (mesmo sem escrever public). Isso garantia que interface era só contrato, sem detalhes de implementação ou estado escondido.

A partir do C# 8, interfaces ficaram um pouco mais "gordas" e ganharam métodos com implementação padrão (Default Interface Methods) e membros estáticos. Isso foi pra poder adicionar métodos novos em interfaces já existentes sem "quebrar" milhões de linhas de código que já usam elas. Mas mesmo assim, interfaces ainda não podem ter campos de instância nem construtores. Essa limitação é chave pra interface continuar sendo "contrato de comportamento", não "armazenamento de estado".


interface ISaveable
{
    void Save(string file); 
    bool IsDirty { get; }
}

interface ILoadable
{
    void Load(string file);
}

class GameProgress : ISaveable, ILoadable
{
    public int Level { get; set; }
    public string PlayerName { get; set; }
    bool _isDirty = true;
    public bool IsDirty => _isDirty;

    public GameProgress(string name, int level)
    {
        PlayerName = name; Level = level;
    }
    public void Save(string file)
    {
        Console.WriteLine($"Salvando: {PlayerName}, nível {Level} -> {file}");
        _isDirty = false;
    }
    public void Load(string file)
    {
        Console.WriteLine($"Carregando de {file}");
        PlayerName = "Novo jogador"; Level = 5; _isDirty = true;
    }
    public void UpdateProgress(int newLevel)
    {
        Level = newLevel; _isDirty = true;
        Console.WriteLine($"Atualizado para {Level}.");
    }
}

class Program
{
    static void Main()
    {
        var game = new GameProgress("Herói", 1);
        game.UpdateProgress(3);

        ISaveable saver = game;
        if (saver.IsDirty) saver.Save("save.dat");

        ILoadable loader = game;
        loader.Load("save.dat");
        Console.WriteLine($"Depois de carregar: {game.PlayerName}, {game.Level}");
    }
}

Resumo: Se você precisa de uma classe base que oferece não só o contrato, mas também código já implementado (lógica comum) ou guarda estado, vai de classe abstrata. Se precisa só de um "checklist" ou "acordo" de comportamento, sem implementação nem estado, interface é o caminho.

4. Quando usar interfaces?

Interfaces são ótimas quando você quer definir uma habilidade ou comportamento que pode estar em objetos totalmente diferentes, sem relação. Tipo:

  • IDisposable: qualquer objeto que precisa ser "descartado" direito depois de usar (arquivo, conexão de rede, banco de dados).
  • IEnumerable<T>: qualquer objeto que pode ser percorrido num foreach.
  • IComparable<T>: qualquer objeto que pode ser comparado com outro do mesmo tipo.

O legal é que FileStream e SqlConnection podem implementar IDisposable, e List<T> e Dictionary<TKey, TValue> podem implementar IEnumerable<T>. São classes de hierarquias totalmente diferentes, mas têm uma habilidade em comum, definida pela interface.

Exemplo: Imagina um sistema onde você tem Carro e Avião. Ambos podem ser Veículo (talvez uma classe abstrata). Mas Carro, Avião e até Barco (se você adicionar) – podem se mover. Essa habilidade "se mover" é um ótimo caso pra interface IMovable.


interface IMovable
{
    void Move(int distance);
}

class Car : IMovable
{
    public string Brand;
    public Car(string brand) => Brand = brand;
    public void Move(int d) => Console.WriteLine($"{Brand} anda {d} km.");
}

class Airplane : IMovable
{
    public string Model;
    public Airplane(string model) => Model = model;
    public void Move(int d) => Console.WriteLine($"{Model} voa {d} km.");
}

class Human : IMovable
{
    public string Name;
    public Human(string name) => Name = name;
    public void Move(int d) => Console.WriteLine($"{Name} caminha {d} m.");
}

class Program
{
    static void Main()
    {
        IMovable[] movers = { new Car("Toyota"), new Airplane("Boeing 747"), new Human("Arthur") };
        foreach (var item in movers)
            item.Move(100);
    }
}

Viu só? Dá pra criar uma lista de IMovable e chamar o método Move() pra cada um, sem saber se é Car, Airplane ou Human. Isso é o poder do polimorfismo via interfaces.

5. Quando usar classes abstratas?

Classes abstratas são ideais quando você quer definir funcionalidade base comum pra um grupo de classes bem relacionadas, que são variações de algo em comum. Elas oferecem código pronto que todos os filhos precisam igual, e ainda obrigam os filhos a implementar as partes específicas.

Imagina que você tem vários tipos de conta bancária: SavingAccount (poupança), CheckingAccount (corrente), CreditAccount (crédito). Todos eles são BankAccount. Todos têm saldo (Balance) e todos podem depositar (Deposit). Mas as regras pra sacar (Withdraw) são diferentes. Aí entra a classe abstrata BankAccount!


abstract class BankAccount
{
    public string AccountNumber { get; }
    public decimal Balance { get; protected set; }
    public BankAccount(string acc) { AccountNumber = acc; }
    public void Deposit(decimal sum)
    {
        if (sum > 0)
        {
            Balance += sum;
            Console.WriteLine($"{AccountNumber}: +{sum:C}, Saldo: {Balance:C}");
        }
    }
    public abstract bool Withdraw(decimal sum);
}

class CheckingAccount : BankAccount
{
    public CheckingAccount(string acc) : base(acc) { }
    public override bool Withdraw(decimal sum)
    {
        if (Balance >= sum)
        {
            Balance -= sum;
            Console.WriteLine($"{AccountNumber}: -{sum:C}, Saldo: {Balance:C}");
            return true;
        }
        Console.WriteLine($"{AccountNumber}: Saldo insuficiente");
        return false;
    }
}

class CreditAccount : BankAccount
{
    public decimal CreditLimit { get; }
    public CreditAccount(string acc, decimal limit) : base(acc) => CreditLimit = limit;
    public override bool Withdraw(decimal sum)
    {
        if (Balance - sum >= -CreditLimit)
        {
            Balance -= sum;
            Console.WriteLine($"{AccountNumber}: -{sum:C}, Saldo: {Balance:C}");
            return true;
        }
        Console.WriteLine($"{AccountNumber}: Limite excedido");
        return false;
    }
}

class Program
{
    static void Main()
    {
        var checking = new CheckingAccount("12345");
        checking.Deposit(1000);
        checking.Withdraw(300);
        checking.Withdraw(800);

        Console.WriteLine("\n--- Conta de crédito ---");
        var credit = new CreditAccount("67890", 500);
        credit.Deposit(200);
        credit.Withdraw(400);
        credit.Withdraw(400);
    }
}

Aqui BankAccount dá pra todas as contas a lógica comum de depósito (Deposit) e gerencia número da conta e saldo. Mas a lógica de saque (Withdraw) é diferente, então ela é abstrata.

6. Quando eles trabalham juntos: o casal perfeito

O design mais elegante e poderoso geralmente mistura classes abstratas e interfaces. Uma classe abstrata pode implementar uma ou várias interfaces!

Imagina: você tem abstract Animal, que define coisas básicas. Mas alguns Animal podem ser IMovable, ICarnivore, IPredator e por aí vai. Seu Animal pode até dar uma implementação base pra IMovable (tipo o método Move(int speed)), mas aí classes concretas como Lion ou Fish vão sobrescrever pra se mover do jeito delas.


public interface ISaveable
{
    void SaveState(string path);
    bool HasChanges { get; }
}

public class Vector3
{
    public float X, Y, Z;
    public Vector3(float x, float y, float z) { X = x; Y = y; Z = z; }
    public override string ToString() => $"({X}, {Y}, {Z})";
}

public abstract class GameObject
{
    public string Id { get; }
    public Vector3 Position { get; }
    protected GameObject(string id, Vector3 pos) { Id = id; Position = pos; }
    public abstract void Update();
    public void Destroy() => Console.WriteLine($"{Id} destruído");
}

public class Player : GameObject, ISaveable
{
    public int Health { get; private set; }
    private bool _hasChanges = true;
    public bool HasChanges => _hasChanges;

    public Player(string id, Vector3 pos, int health) : base(id, pos) => Health = health;

    public override void Update() =>
        Console.WriteLine($"{Id} atualizando, HP: {Health}, Pos: {Position}");

    public void TakeDamage(int dmg)
    {
        Health -= dmg;
        _hasChanges = true;
        Console.WriteLine($"{Id} levou {dmg} de dano. HP: {Health}");
    }

    public void SaveState(string path)
    {
        Console.WriteLine($"Salvando {Id} (HP:{Health}) em {path}");
        _hasChanges = false;
    }
}

class GameEngine
{
    static void Main()
    {
        var player = new Player("P1", new Vector3(0, 0, 0), 100);
        player.Update();
        player.TakeDamage(20);

        ISaveable saver = player;
        if (saver.HasChanges) saver.SaveState("save.json");

        GameObject obj = player;
        obj.Update();
        player.Destroy();
    }
}

Nesse exemplo, Player é um GameObject (porque herda dele, usando os campos Id, Position e o método Destroy). E ao mesmo tempo Player pode ser salvo (porque implementa a interface ISaveable). Bem flexível e poderoso!

7. Detalhes e erros comuns

Tentar criar instância de classe abstrata:

Animal myAnimal = new Animal();
– erro clássico de iniciante. Lembra: classe abstrata é molde, não objeto pronto. O compilador já vai reclamar.

Esquecer de implementar métodos abstratos: Se você herda de uma classe abstrata e sua classe não é abstrata, tem que sobrescrever (override) todos os métodos abstratos da base. Senão o compilador vai reclamar também.

Tentar colocar campos em interface: Isso é um dos "não pode" que diferencia interface. No C# 8+ tem campos estáticos, mas campos de instância (ou seja, que pertencem ao objeto) continuam proibidos. Interface é contrato de comportamento, não armazenamento de dados.

Confundir virtual e abstract:

  • Método/propriedade virtual tem implementação padrão e pode ser sobrescrito nos filhos.
  • Método/propriedade abstract não tem implementação e deve ser sobrescrito no primeiro filho não abstrato.
  • Usar virtual onde a lógica sempre muda faz você escrever implementação vazia só pra depois sobrescrever. O contrato fica menos claro do que com abstract.

Sacar essas diferenças e escolher certo entre classe abstrata e interface é uma skill chave pra todo dev C#. Não é só questão de sintaxe, é questão de arquitetura. Quando for projetar um sistema, se pergunte: "Essas classes têm uma hierarquia comum de 'é um'?" e "Essas classes têm uma 'habilidade' comum que serve pra tipos não relacionados?". As respostas vão te ajudar a decidir.

Nas próximas aulas a gente vai continuar explorando o mundo das interfaces, vendo recursos mais avançados que chegaram nas versões novas do C#. Fica ligado!

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION