C# Interfaces

Uma classe abstrata junta uma família: retângulo e círculo são formas. Mas às vezes coisas que não têm nada de parentes precisam saber fazer a mesma coisa: um pássaro, um avião e um drone voam. Uma conta de luz, um boleto e um aluguel são pagos. Para isso existem as interfaces: um contrato que diz "quem assinar aqui sabe fazer isto", sem se importar com a família de cada um.

Você vai aprender
  • O que é uma interface (interface) e a convenção do I no nome.
  • Como uma classe implementa uma interface (class Aviao : IVoador) e por que o método tem de ser public.
  • Os erros que o compilador cobra: CS0535, CS0737, CS0144 e CS0525.
  • Propriedades na interface.
  • Uma classe com uma mãe e várias interfaces ao mesmo tempo.
  • Métodos padrão (código pronto na interface), que só aparecem pela interface.
  • Como perguntar se um objeto tem uma habilidade (is) e quando usar interface ou classe abstrata.

Todos os programas desta aula rodaram com dotnet run arquivo.cs no .NET 10 da máquina da Ana, na pasta ~/cs.


A ideia: uma tomada

A tomada da parede é um contrato: qualquer aparelho com o plugue certo funciona nela. A tomada não sabe se você vai ligar um abajur, uma geladeira ou um carregador de celular, e nem precisa saber. Ela só exige o plugue. Do outro lado, o fabricante do abajur não precisa conhecer a sua casa: basta fazer o plugue no formato combinado.

Uma interface é essa tomada:

  • ela diz o que tem de existir (os métodos e as propriedades), mas não como;
  • cada classe que a implementa escreve o "como" do seu jeito;
  • e o resto do programa pode trabalhar com "qualquer coisa que tenha esse plugue".

Interface, em inglês, é "a superfície de contato entre duas coisas": o que um lado mostra para o outro.


interface e a implementação

IVoador[] voadores = [new Passaro("Piu"), new Aviao("PT-ANA")];
foreach (IVoador v in voadores)
{
    v.Voar();
}

interface IVoador
{
    void Voar();
}

class Passaro : IVoador
{
    public string Nome { get; }

    public Passaro(string nome)
    {
        Nome = nome;
    }

    public void Voar()
    {
        Console.WriteLine($"{Nome} bate as asas.");
    }
}

class Aviao : IVoador
{
    public string Prefixo { get; }

    public Aviao(string prefixo)
    {
        Prefixo = prefixo;
    }

    public void Voar()
    {
        Console.WriteLine($"O avião {Prefixo} liga as turbinas.");
    }
}
interface IVoador
Declara uma interface chamada IVoador ("voador"). Em C#, todo nome de interface começa com I maiúsculo, seguido de PascalCase: IVoador, IPagavel, IComparable, IDisposable. Não é obrigatório para o compilador, mas é a convenção de todo código C#, e ajuda a ver na hora que aquilo é uma interface.
void Voar();
Um membro da interface: só a assinatura, terminada em ;. Não precisa de public nem de abstract: dentro da interface, os membros já são públicos e sem corpo.
class Passaro : IVoador
"Passaro implementa IVoador". É o mesmo : da herança. A classe assina o contrato: promete ter um Voar().
public void Voar() { … }
A implementação. Tem de ser public e ter a mesma assinatura. Repare: aqui não se escreve override (isso é só para virtual e abstract de uma classe mãe).
IVoador[] voadores
Um array do tipo da interface. Ele aceita qualquer objeto de uma classe que implemente IVoador, por mais diferentes que sejam.
v.Voar();
Para cada objeto, o C# chama o Voar() da classe dele. É o polimorfismo, agora sem herança nenhuma: Passaro e Aviao não são parentes.
$ dotnet run voadores.cs
Piu bate as asas.
O avião PT-ANA liga as turbinas.

Missão: crie a classe Drone, que implementa IVoador, com um Voar() que escreve a frase O drone gira as hélices. e coloque um new Drone() no array. Depois, para ver o compilador cobrando o contrato, rode uma vez com o Drone sem o método.

IVoador[] voadores = [new Passaro("Piu")];
foreach (IVoador v in voadores)
{
    v.Voar();
}

interface IVoador
{
    void Voar();
}

class Passaro : IVoador
{
    public string Nome { get; }

    public Passaro(string nome)
    {
        Nome = nome;
    }

    public void Voar()
    {
        Console.WriteLine($"{Nome} bate as asas.");
    }
}

// crie aqui a classe Drone
IVoador[] voadores = [new Passaro("Piu"), new Drone()];
foreach (IVoador v in voadores)
{
    v.Voar();
}

interface IVoador
{
    void Voar();
}

class Passaro : IVoador
{
    public string Nome { get; }

    public Passaro(string nome)
    {
        Nome = nome;
    }

    public void Voar()
    {
        Console.WriteLine($"{Nome} bate as asas.");
    }
}

class Drone : IVoador
{
    public void Voar()
    {
        Console.WriteLine("O drone gira as hélices.");
    }
}

As regras que o compilador cobra

1. Assinou, tem de cumprir

A Ana apagou o Voar() do Aviao:

$ dotnet run semvoar.cs
/home/ana/cs/semvoar.cs(27,15): error CS0535: "Aviao" não implementa membro de interface "IVoador.Voar()"

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

A posição (27,15) aponta o IVoador da linha class Aviao : IVoador: o contrato que não foi cumprido. Em inglês: 'Aviao' does not implement interface member 'IVoador.Voar()'.

2. A implementação tem de ser public

Agora o método existe, mas a Ana esqueceu o public (sem ele, o método é private):

$ dotnet run sempublic.cs
/home/ana/cs/sempublic.cs(27,15): error CS0737: "Aviao" não implementa membro de interface "IVoador.Voar()". "Aviao.Voar()" não pode implementar um membro de interface, pois não é público.

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

Faz sentido: o contrato promete que qualquer um pode chamar Voar(). Um método escondido não cumpre a promessa. Escreva public void Voar().

3. Interface não vira objeto

Como a classe abstrata, uma interface não tem new: não existe "um voador genérico".

$ dotnet run newif.cs
/home/ana/cs/newif.cs(1,14): error CS0144: Não é possível criar uma instância do tipo abstrato ou da interface 'IVoador'

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

A linha era IVoador v1 = new IVoador();. A variável do tipo IVoador é permitida; o que falta é escolher uma classe de verdade: IVoador v1 = new Passaro("Piu");.

4. Interface não guarda dados

Uma interface descreve o que o objeto sabe fazer, não o que ele guarda. Um campo, como int altitude;, é proibido:

$ dotnet run campo.cs
/home/ana/cs/campo.cs(9,9): error CS0525: As interfaces não podem conter campos de instância

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

Se todo voador precisa informar a altitude, declare uma propriedade: int Altitude { get; }. Cada classe decide onde guardar o valor.


Propriedades na interface

A interface pode exigir propriedades, além de métodos. A Ana quer somar as contas do mês, que são de tipos bem diferentes:

List<IPagavel> contas = [new ContaDeLuz(150.75m), new Boleto("Internet", 99.90m)];
decimal total = 0;
foreach (IPagavel c in contas)
{
    Console.WriteLine($"{c.Descricao}: {c.Valor:C}");
    total += c.Valor;
}
Console.WriteLine($"Total: {total:C}");

interface IPagavel
{
    string Descricao { get; }
    decimal Valor { get; }
}

class ContaDeLuz : IPagavel
{
    public string Descricao => "Conta de luz";
    public decimal Valor { get; }

    public ContaDeLuz(decimal valor)
    {
        Valor = valor;
    }
}

class Boleto : IPagavel
{
    public string Descricao { get; }
    public decimal Valor { get; }

    public Boleto(string descricao, decimal valor)
    {
        Descricao = descricao;
        Valor = valor;
    }
}
string Descricao { get; }
"Todo pagável tem uma descrição que se pode ler." Na interface, o { get; } não cria nada: só exige.
public string Descricao => "Conta de luz";
A ContaDeLuz cumpre com uma propriedade calculada: toda conta de luz tem a mesma descrição.
public string Descricao { get; }
O Boleto cumpre com uma propriedade automática, preenchida no construtor. As duas formas valem: a interface só exige que exista um get público.
{c.Valor:C}
Formato de moeda (C, de currency), que na cultura pt-BR mostra R$.
$ dotnet run contas.cs
Conta de luz: R$ 150,75
Internet: R$ 99,90
Total: R$ 250,65

O laço não sabe e não quer saber o que é cada conta. Amanhã a Ana pode criar Aluguel, Mensalidade, Fatura: se a classe assinar IPagavel, o laço funciona sem mudar uma linha.


Uma mãe e várias interfaces

Uma classe só pode ter uma classe mãe, mas pode implementar quantas interfaces quiser. O pato é um animal (herança), voa e nada (duas habilidades):

Pato donald = new Pato("Donald");
donald.Voar();
donald.Nadar();
donald.Comer();

IVoador v = donald;
INadador n = donald;
v.Voar();
n.Nadar();

interface IVoador
{
    void Voar();
}

interface INadador
{
    void Nadar();
}

class Animal
{
    public string Nome { get; }

    public Animal(string nome)
    {
        Nome = nome;
    }

    public void Comer()
    {
        Console.WriteLine($"{Nome} come.");
    }
}

class Pato : Animal, IVoador, INadador
{
    public Pato(string nome) : base(nome) { }

    public void Voar() => Console.WriteLine($"{Nome} voa baixinho.");
    public void Nadar() => Console.WriteLine($"{Nome} nada no lago.");
}
class Pato : Animal, IVoador, INadador
Depois do :, uma lista separada por vírgulas: primeiro a classe mãe (se houver), depois as interfaces, em qualquer ordem.
public void Voar() => …
Um método de uma linha só, com => (veja Métodos). Cumpre o contrato de IVoador.
IVoador v = donald;
O mesmo pato, visto como "um voador". Pela variável v, só dá para usar o que IVoador tem: v.Voar().
$ dotnet run pato.cs
Donald voa baixinho.
Donald nada no lago.
Donald come.
Donald voa baixinho.      « pela variável IVoador
Donald nada no lago.      « pela variável INadador

Duas coisas que dão erro aqui. Se a interface vier antes da mãe (class Pato : IVoador, Animal, INadador):

$ dotnet run ordem.cs
/home/ana/cs/ordem.cs(36,23): error CS1722: Classe base "Animal" deve vir antes de quaisquer interfaces

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

E, se a Ana tentar v.Nadar() pela variável do tipo IVoador:

$ dotnet run visao.cs
/home/ana/cs/visao.cs(9,3): error CS1061: ‘IVoador’ não contém uma definição para "Nadar" e não foi possível encontrar nenhum método de extensão "Nadar" que aceite um primeiro argumento do tipo ‘IVoador’ (você está se esquecendo de usar uma diretiva ou uma referência de assembly?)

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O objeto é um pato e sabe nadar, mas a variável é uma "janela" do tipo IVoador, e por essa janela só se vê o Voar(). O compilador olha o tipo da variável, não o do objeto. Guarde essa ideia: ela volta já, nos métodos padrão.


Métodos padrão: código pronto na interface C# 8

Desde o C# 8 (.NET Core 3.0, 2019), uma interface pode trazer um método com corpo, chamado método padrão (em inglês, default interface method). Quem implementa a interface ganha esse método pronto e não precisa escrevê-lo. Um aparelho que liga e desliga sabe "reiniciar" sem esforço: é desligar e ligar.

Lampada sala = new Lampada();
sala.Ligar();

ILigavel l = sala;
l.Ligar();
l.Reiniciar();

interface ILigavel
{
    void Ligar();
    void Desligar();

    void Reiniciar()
    {
        Desligar();
        Ligar();
    }
}

class Lampada : ILigavel
{
    public void Ligar() => Console.WriteLine("Lâmpada acesa.");
    public void Desligar() => Console.WriteLine("Lâmpada apagada.");
}
void Ligar(); void Desligar();
Membros comuns: sem corpo, toda classe tem de escrever.
void Reiniciar() { Desligar(); Ligar(); }
O método padrão: tem corpo, dentro da interface. Ele usa os outros membros da interface, sem saber como cada classe os escreveu.
class Lampada : ILigavel
A lâmpada só escreve Ligar e Desligar. O Reiniciar vem de graça.
ILigavel l = sala; l.Reiniciar();
O método padrão é chamado pela interface. (Veja abaixo por quê.)
$ dotnet run padrao.cs
Lâmpada acesa.        « sala.Ligar()
Lâmpada acesa.        « l.Ligar()
Lâmpada apagada.      « l.Reiniciar(): Desligar…
Lâmpada acesa.        « …e Ligar

A pegadinha: o método padrão não aparece na classe

Quem vem do Java estranha: em C#, o método padrão não é herdado pela classe. Ele pertence à interface. Se a Ana chamar sala.Reiniciar(), com a variável do tipo Lampada:

$ dotnet run padrao-erro.cs
/home/ana/cs/padrao-erro.cs(7,6): error CS1061: ‘Lampada’ não contém uma definição para "Reiniciar" e não foi possível encontrar nenhum método de extensão "Reiniciar" que aceite um primeiro argumento do tipo ‘Lampada’ (você está se esquecendo de usar uma diretiva ou uma referência de assembly?)

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

"Lampada não contém uma definição para Reiniciar": é verdade, a classe nunca escreveu esse método. Para usá-lo, olhe o objeto pela "janela" da interface: ILigavel l = sala; l.Reiniciar();. Na prática, isso quase nunca incomoda: quem usa interfaces costuma guardar os objetos em variáveis, listas e parâmetros do tipo da interface.

A classe pode escrever a própria versão

O padrão é só um padrão. Se a classe tem um método public com a mesma assinatura, é ele que vale, também pela interface. O computador precisa salvar os arquivos antes de reiniciar:

ILigavel[] aparelhos = [new Lampada(), new Computador()];
foreach (ILigavel a in aparelhos)
{
    a.Reiniciar();
    Console.WriteLine("---");
}

interface ILigavel
{
    void Ligar();
    void Desligar();

    void Reiniciar()
    {
        Desligar();
        Ligar();
    }
}

class Lampada : ILigavel
{
    public void Ligar() => Console.WriteLine("Lâmpada acesa.");
    public void Desligar() => Console.WriteLine("Lâmpada apagada.");
}

class Computador : ILigavel
{
    public void Ligar() => Console.WriteLine("Computador ligado.");
    public void Desligar() => Console.WriteLine("Computador desligado.");

    public void Reiniciar()
    {
        Console.WriteLine("Salvando os arquivos...");
        Desligar();
        Ligar();
    }
}
$ dotnet run aparelhos.cs
Lâmpada apagada.
Lâmpada acesa.
---
Salvando os arquivos...       « a versão do Computador
Computador desligado.
Computador ligado.
---
Para que servem os métodos padrão?

O motivo original é evoluir uma interface sem quebrar ninguém. Se a interface ILigavel já é usada por 50 classes e alguém acrescenta um void Reiniciar(); sem corpo, as 50 deixam de compilar (CS0535). Com um corpo padrão, todas continuam funcionando, e quem quiser escreve a sua versão. Use com moderação: a maior parte das interfaces do dia a dia só tem membros sem corpo.


Esse objeto sabe voar? O is

Às vezes você recebe um objeto qualquer e quer saber se ele tem certa habilidade. O is testa e, se der certo, já entrega uma variável do tipo da interface (veja mais em Pattern Matching):

object[] coisas = [new Passaro(), "texto", new Pedra()];
foreach (object o in coisas)
{
    if (o is IVoador v)
    {
        v.Voar();
    }
    else
    {
        Console.WriteLine($"{o} não voa.");
    }
}

interface IVoador
{
    void Voar();
}

class Passaro : IVoador
{
    public void Voar() => Console.WriteLine("O pássaro voa.");
}

class Pedra
{
    public override string ToString() => "A pedra";
}
object[] coisas
Um array de object, o tipo que aceita qualquer coisa (veja A Classe object).
o is IVoador v
"o implementa IVoador? Se sim, chame-o de v." Dentro do if, v é um IVoador e tem Voar().
public override string ToString()
Diz como a pedra aparece num texto. É o assunto da aula A Classe object.
$ dotnet run is.cs
O pássaro voa.
texto não voa.
A pedra não voa.

Interface × classe abstrata

Classe abstrataInterface
IdeiaUma família ("é um")Uma habilidade ("sabe fazer")
Quantas uma classe pode terUma sóQuantas quiser
Campos (dados guardados)SimNão (CS0525)
ConstrutorSimNão
Membros sem corpoabstract, com override na filhaO normal, sem override na classe
Membros com corpoSim, herdados pelas filhasMétodos padrão C# 8, vistos só pela interface
Instanciar com newNão (CS0144)Não (CS0144)
NomeForma, FuncionarioIVoador, IPagavel

Qual usar? Comece pela pergunta: "as classes são parentes, com dados e código em comum?" Se sim, classe abstrata. "Coisas diferentes precisam de uma mesma habilidade?" Interface. Na dúvida, prefira a interface: ela não "gasta" a única mãe da classe, e as duas podem andar juntas (class Pato : Animal, IVoador).

As interfaces da biblioteca

O .NET está cheio de interfaces, e você vai implementá-las nas próximas aulas: IComparable<T> ("sei me comparar", para ordenar: veja IComparable e IComparer), IDisposable ("tenho algo para fechar": veja using e IDisposable) e IEnumerable<T> ("posso ser percorrido com foreach": veja IEnumerable e foreach).


Quando dá errado

CS0535: "… não implementa membro de interface …"
$ dotnet run semvoar.cs
/home/ana/cs/semvoar.cs(27,15): error CS0535: "Aviao" não implementa membro de interface "IVoador.Voar()"

O que aconteceu: a classe assinou a interface e não escreveu um dos membros (ou escreveu com outro nome ou outros parâmetros).

Como resolver: escreva o membro com a assinatura exata e public. Num editor como o VS Code, a lâmpada de "ações rápidas" sobre o erro oferece "Implementar interface", que cria os esqueletos.

CS0737: "… não pode implementar um membro de interface, pois não é público"
$ dotnet run sempublic.cs
/home/ana/cs/sempublic.cs(27,15): error CS0737: "Aviao" não implementa membro de interface "IVoador.Voar()". "Aviao.Voar()" não pode implementar um membro de interface, pois não é público.

O que aconteceu: o método existe, mas sem public.

Como resolver: acrescente public.

CS1061 num método padrão
$ dotnet run padrao-erro.cs
/home/ana/cs/padrao-erro.cs(7,6): error CS1061: ‘Lampada’ não contém uma definição para "Reiniciar" …

O que aconteceu: o método padrão foi chamado por uma variável do tipo da classe.

Como resolver: use uma variável do tipo da interface (ILigavel l = sala;) ou escreva o método na classe, se ela precisar dele diretamente.

CS1722: "Classe base … deve vir antes de quaisquer interfaces"
$ dotnet run ordem.cs
/home/ana/cs/ordem.cs(36,23): error CS1722: Classe base "Animal" deve vir antes de quaisquer interfaces

Como resolver: a mãe primeiro: class Pato : Animal, IVoador, INadador.


Referência

EscritaSignificado
interface IVoador { void Voar(); }Declara uma interface com um método (público e sem corpo)
decimal Valor { get; }Propriedade exigida pela interface
class Aviao : IVoadorA classe implementa a interface
class Pato : Animal, IVoador, INadadorUma mãe (primeiro) e várias interfaces
public void Voar() { … }Implementação: public, mesma assinatura, sem override
void Reiniciar() { … } na interfaceMétodo padrão C# 8; chamado pela interface
IVoador v = new Aviao("PT-ANA");Variável do tipo da interface
if (o is IVoador v)Testa se o objeto implementa a interface
Nome começando com IConvenção de todo código C#

Exercícios

1. Declarar a interface

Complete para declarar a interface INadador:

INadador { void Nadar(); }

2. Assinar o contrato

Complete para a classe Peixe implementar INadador:

class Peixe

3. O modificador obrigatório

Complete a primeira linha do método que cumpre o contrato:

void Nadar()

4. Qual destas declarações compila?

5. ILigavel tem o método padrão Reiniciar(), e Lampada implementa ILigavel sem escrever Reiniciar. Com Lampada sala = new Lampada();, o que acontece em sala.Reiniciar();?

6. O que uma interface não pode ter?

Missão final: a Ana quer incluir o aluguel nas contas do mês. Crie a classe Aluguel, que implementa IPagavel: a Descricao é sempre "Aluguel" e o Valor é sempre 1200m (use propriedades com =>). Coloque um new Aluguel() na lista. A última linha deve ser Total: R$ 1.299,90.

List<IPagavel> contas = [new Boleto("Internet", 99.90m)];
decimal total = 0;
foreach (IPagavel c in contas)
{
    Console.WriteLine($"{c.Descricao}: {c.Valor:C}");
    total += c.Valor;
}
Console.WriteLine($"Total: {total:C}");

interface IPagavel
{
    string Descricao { get; }
    decimal Valor { get; }
}

class Boleto : IPagavel
{
    public string Descricao { get; }
    public decimal Valor { get; }

    public Boleto(string descricao, decimal valor)
    {
        Descricao = descricao;
        Valor = valor;
    }
}

// crie aqui a classe Aluguel
List<IPagavel> contas = [new Boleto("Internet", 99.90m), new Aluguel()];
decimal total = 0;
foreach (IPagavel c in contas)
{
    Console.WriteLine($"{c.Descricao}: {c.Valor:C}");
    total += c.Valor;
}
Console.WriteLine($"Total: {total:C}");

interface IPagavel
{
    string Descricao { get; }
    decimal Valor { get; }
}

class Boleto : IPagavel
{
    public string Descricao { get; }
    public decimal Valor { get; }

    public Boleto(string descricao, decimal valor)
    {
        Descricao = descricao;
        Valor = valor;
    }
}

class Aluguel : IPagavel
{
    public string Descricao => "Aluguel";
    public decimal Valor => 1200m;
}
$ dotnet run aluguel.cs      « a solução
Internet: R$ 99,90
Aluguel: R$ 1.200,00
Total: R$ 1.299,90

Resumo

  • Uma interface é um contrato: diz o que uma classe sabe fazer, sem dizer como. O nome começa com I: IVoador, IPagavel.
  • class Aviao : IVoador assina o contrato. Cada membro tem de ser escrito, public e com a mesma assinatura (CS0535, CS0737), sem override.
  • Interfaces podem exigir métodos e propriedades, mas não guardam campos (CS0525) e não viram objeto (CS0144).
  • Uma classe tem uma mãe só, mas quantas interfaces quiser: class Pato : Animal, IVoador, INadador, com a mãe primeiro (CS1722).
  • Métodos padrão C# 8 trazem código pronto na interface. Eles são chamados pela interface, não pela classe (CS1061), e a classe pode escrever a própria versão.
  • o is IVoador v pergunta se o objeto tem a habilidade. Família com dados em comum: classe abstrata. Habilidade compartilhada: interface. Próxima aula: sealed.