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.
- O que é uma interface (
interface) e a convenção doIno nome. - Como uma classe implementa uma interface (
class Aviao : IVoador) e por que o método tem de serpublic. - Os erros que o compilador cobra:
CS0535,CS0737,CS0144eCS0525. - 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 comImaiú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 depublicnem deabstract: dentro da interface, os membros já são públicos e sem corpo. - class Passaro : IVoador
- "
PassaroimplementaIVoador". É o mesmo:da herança. A classe assina o contrato: promete ter umVoar(). - public void Voar() { … }
- A implementação. Tem de ser
publice ter a mesma assinatura. Repare: aqui não se escreveoverride(isso é só paravirtualeabstractde 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:PassaroeAviaonã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
ContaDeLuzcumpre com uma propriedade calculada: toda conta de luz tem a mesma descrição. - public string Descricao { get; }
- O
Boletocumpre com uma propriedade automática, preenchida no construtor. As duas formas valem: a interface só exige que exista umgetpúblico. - {c.Valor:C}
- Formato de moeda (
C, de currency), que na cultura pt-BR mostraR$.
$ 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 deIVoador. - IVoador v = donald;
- O mesmo pato, visto como "um voador". Pela variável
v, só dá para usar o queIVoadortem: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
LigareDesligar. OReiniciarvem 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. ---
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
- "
oimplementaIVoador? Se sim, chame-o dev." Dentro doif,vé umIVoadore temVoar(). - 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 abstrata | Interface | |
|---|---|---|
| Ideia | Uma família ("é um") | Uma habilidade ("sabe fazer") |
| Quantas uma classe pode ter | Uma só | Quantas quiser |
| Campos (dados guardados) | Sim | Não (CS0525) |
| Construtor | Sim | Não |
| Membros sem corpo | abstract, com override na filha | O normal, sem override na classe |
| Membros com corpo | Sim, herdados pelas filhas | Métodos padrão C# 8, vistos só pela interface |
Instanciar com new | Não (CS0144) | Não (CS0144) |
| Nome | Forma, Funcionario | IVoador, 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).
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
$ 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.
$ 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.
$ 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.
$ 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
| Escrita | Significado |
|---|---|
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 : IVoador | A classe implementa a interface |
class Pato : Animal, IVoador, INadador | Uma mãe (primeiro) e várias interfaces |
public void Voar() { … } | Implementação: public, mesma assinatura, sem override |
void Reiniciar() { … } na interface | Mé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 I | Convenção de todo código C# |
Exercícios
1. Declarar a interface
Complete para declarar a interface INadador:
2. Assinar o contrato
Complete para a classe Peixe implementar INadador:
3. O modificador obrigatório
Complete a primeira linha do método que cumpre o contrato:
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 comI:IVoador,IPagavel. class Aviao : IVoadorassina o contrato. Cada membro tem de ser escrito,publice com a mesma assinatura (CS0535,CS0737), semoverride.- 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 vpergunta se o objeto tem a habilidade. Família com dados em comum: classe abstrata. Habilidade compartilhada: interface. Próxima aula: sealed.