C# Classes Abstratas
Na aula de Polimorfismo, a classe Forma tinha um método Area() que devolvia 0. Era uma gambiarra: "forma" é uma ideia geral, sem área definida. Só um retângulo ou um círculo de verdade têm área. As classes abstratas são o jeito certo de escrever essas ideias gerais: uma classe que serve de modelo para as filhas, que não pode virar objeto sozinha e que pode obrigar as filhas a escrever certos métodos.
- O que é uma classe abstrata (
abstract class) e um método abstrato. - Por que não dá para criar um objeto de uma classe abstrata (o erro
CS0144). - Como o compilador obriga as classes filhas a implementar os membros abstratos (
CS0534). - Propriedades abstratas e o construtor
protected. - Como misturar membros abstratos e métodos prontos na mesma classe.
- Quando usar uma classe abstrata e quando usar uma interface.
Todos os programas desta aula rodaram com dotnet run arquivo.cs (veja Rodar com dotnet run) no .NET 10 da máquina da Ana, na pasta ~/cs. As mensagens do compilador estão em português, como no Ubuntu em pt-BR.
A ideia: "instrumento musical"
Numa loja de música, você pode comprar um violão, um piano, uma flauta. Mas não pode comprar "um instrumento musical", genérico. "Instrumento" é uma categoria: diz o que todos têm em comum (todo instrumento toca, tem um nome, tem um preço), mas não existe sozinho. E, embora todo instrumento toque, cada um toca de um jeito: não dá para escrever o "tocar" de um instrumento genérico.
Em C#:
- A categoria é uma classe abstrata: não se cria objeto dela (
new Instrumento()é proibido). - O "todo instrumento toca, mas cada um do seu jeito" é um membro abstrato: declarado na classe abstrata, sem corpo, e escrito por cada classe filha com
override.
A palavra abstract ("abstrato") vem do latim abstractus, "separado, retirado": algo pensado em geral, separado dos casos concretos. O oposto é concreto: uma classe comum, de que se pode criar objetos.
Uma Forma abstrata
List<Forma> formas = [new Retangulo("Tapete", 2, 3), new Circulo("Mesa", 1)];
foreach (Forma f in formas)
{
Console.WriteLine(f.Descrever());
}
abstract class Forma
{
public string Nome { get; }
protected Forma(string nome)
{
Nome = nome;
}
public abstract double Area();
public string Descrever()
{
return $"{Nome}: área {Area():F2}";
}
}
class Retangulo : Forma
{
public double Largura { get; }
public double Altura { get; }
public Retangulo(string nome, double largura, double altura) : base(nome)
{
Largura = largura;
Altura = altura;
}
public override double Area()
{
return Largura * Altura;
}
}
class Circulo : Forma
{
public double Raio { get; }
public Circulo(string nome, double raio) : base(nome)
{
Raio = raio;
}
public override double Area()
{
return Math.PI * Raio * Raio;
}
}
- abstract class Forma
- Uma classe abstrata. Pode ter campos, propriedades, construtores e métodos como qualquer classe, mas não pode ser instanciada com
new. - public string Nome { get; }
- Uma propriedade comum (veja Propriedades). Toda forma tem nome, e as filhas herdam essa propriedade pronta.
- protected Forma(string nome)
- Sim, classe abstrata tem construtor! Ele monta a "parte
Forma" de cada objeto e é chamado pelas filhas com: base(nome)(veja base). Ele éprotected("protegido"): só as filhas o enxergam. Como ninguém de fora pode fazernew Forma, um construtorpublicnão teria utilidade. - public abstract double Area();
- Um método abstrato: só a assinatura, terminada em
;, sem chaves e sem corpo. Ele diz "toda forma tem área", sem dizer como calcular. - public string Descrever()
- Um método comum (concreto), pronto, que todas as filhas herdam. Repare que ele usa
Area(), mesmo sem saber como ela é calculada. O:F2mostra 2 casas decimais (veja Formatar Números). - class Retangulo : Forma
- O
:quer dizer "herda de" (veja Herança). - public override double Area()
- Em cada filha, a versão concreta.
override= "sobrepor, substituir". Escrever um membro abstrato herdado também se chama implementar o membro. Em C#, ooverrideé obrigatório: sem ele, o compilador não liga o método da filha ao da mãe (você verá o erro mais abaixo).
$ dotnet run formas.cs Tapete: área 6,00 Mesa: área 3,14 « π × 1 × 1, com 2 casas
Veja como as peças se encaixam: o Descrever() foi escrito uma vez, em Forma. Ao rodar, ele chama Area(), e o polimorfismo escolhe a versão do objeto: a do retângulo para o tapete, a do círculo para a mesa. A classe abstrata escreve o "roteiro"; as filhas preenchem a parte que muda.
Um membro abstrato funciona como um virtual (veja virtual e override) sem versão padrão: o virtual diz "as filhas podem trocar"; o abstract diz "as filhas têm de escrever".
Missão: crie a classe Quadrado, que herda de Forma, recebe nome e lado no construtor (chamando : base(nome)) e implementa Area() como Lado * Lado. Acrescente new Quadrado("Azulejo", 2) à lista. A saída deve ter Azulejo: área 4,00. Para ver a obrigação em ação, rode também uma vez com a classe Quadrado sem o método Area().
List<Forma> formas = [new Retangulo("Tapete", 2, 3)];
foreach (Forma f in formas)
{
Console.WriteLine(f.Descrever());
}
abstract class Forma
{
public string Nome { get; }
protected Forma(string nome)
{
Nome = nome;
}
public abstract double Area();
public string Descrever()
{
return $"{Nome}: área {Area():F2}";
}
}
class Retangulo : Forma
{
public double Largura { get; }
public double Altura { get; }
public Retangulo(string nome, double largura, double altura) : base(nome)
{
Largura = largura;
Altura = altura;
}
public override double Area()
{
return Largura * Altura;
}
}
// crie aqui a classe Quadrado
List<Forma> formas = [new Retangulo("Tapete", 2, 3), new Quadrado("Azulejo", 2)];
foreach (Forma f in formas)
{
Console.WriteLine(f.Descrever());
}
abstract class Forma
{
public string Nome { get; }
protected Forma(string nome)
{
Nome = nome;
}
public abstract double Area();
public string Descrever()
{
return $"{Nome}: área {Area():F2}";
}
}
class Retangulo : Forma
{
public double Largura { get; }
public double Altura { get; }
public Retangulo(string nome, double largura, double altura) : base(nome)
{
Largura = largura;
Altura = altura;
}
public override double Area()
{
return Largura * Altura;
}
}
class Quadrado : Forma
{
public double Lado { get; }
public Quadrado(string nome, double lado) : base(nome)
{
Lado = lado;
}
public override double Area()
{
return Lado * Lado;
}
}
Não dá para instanciar
Uma forma genérica não tem área. Se fosse possível criar uma, o que Descrever() mostraria? O compilador corta o problema pela raiz. A Ana trocou as primeiras linhas do programa por esta:
Forma f = new Forma("Coisa");
// … as classes Forma, Retangulo e Circulo de antes
$ dotnet run instancia.cs /home/ana/cs/instancia.cs(1,11): error CS0144: Não é possível criar uma instância do tipo abstrato ou da interface 'Forma' Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O (1,11) é a linha 1, coluna 11: exatamente onde começa o new Forma. Em inglês, a mensagem é Cannot create an instance of the abstract type or interface 'Forma' ("instanciar" = criar uma instância, um objeto). Repare que o problema é só o new. A variável do tipo Forma é permitida, e é muito útil: Forma f = new Circulo("Mesa", 1); funciona, e é assim que o polimorfismo usa as classes abstratas.
Com abstract class Forma | Pode? |
|---|---|
new Forma("x") | Não (CS0144) |
Forma f = new Circulo("Mesa", 1); | Sim |
List<Forma>, Forma[], void Desenhar(Forma f) | Sim |
class Circulo : Forma | Sim (é para isso que ela existe) |
A obrigação das filhas
O membro abstrato é uma promessa: "toda forma concreta terá Area()". Por isso, uma filha concreta que esquece de implementá-lo não compila. A Ana apagou o Area() do Circulo:
$ dotnet run semarea.cs /home/ana/cs/semarea.cs(41,7): error CS0534: "Circulo" não implementa membro abstrato herdado "Forma.Area()" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
"Circulo não implementa o membro abstrato herdado Forma.Area()" (em inglês, 'Circulo' does not implement inherited abstract member 'Forma.Area()'). A linha 41 é a do class Circulo : Forma. Há duas saídas:
- Implementar o método (o caso normal): escrever
public override double Area()noCirculo. - Declarar a filha também abstrata:
abstract class Poligono : Formapode deixarArea()para as filhas dela (Triangulo,Hexagono…). A obrigação só é cobrada da primeira classe concreta da família.
Essa é a grande vantagem sobre a "gambiarra" do return 0 da aula anterior: lá, quem esquecesse de sobrescrever teria uma área zero, em silêncio. Aqui, o programa nem compila.
Esqueceu o override
Um erro muito comum de quem vem do Java ou do Python: escrever o método certo, mas sem a palavra override.
class Circulo : Forma
{
// …
public double Area() // falta o override
{
return Math.PI * Raio * Raio;
}
}
$ dotnet run semoverride.cs /home/ana/cs/semoverride.cs(50,19): warning CS0114: "Circulo.Area()" oculta o membro herdado "Forma.Area()". Para que o membro atual substitua essa implementação, adicione a palavra-chave override. Caso contrário, adicione a palavra-chave new. /home/ana/cs/semoverride.cs(41,7): error CS0534: "Circulo" não implementa membro abstrato herdado "Forma.Area()" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Duas mensagens. O aviso CS0114 explica o que houve: sem override, o Area() do círculo é um método novo, que só tem o mesmo nome e "esconde" o da mãe. Então o membro abstrato continua sem implementação, e vem o erro CS0534. O conserto está no próprio aviso: "adicione a palavra-chave override".
Propriedades abstratas
Não são só métodos: uma propriedade também pode ser abstrata. Ela declara o get (e, se quiser, o set) sem corpo, e cada filha diz o valor. Assim fica o exemplo dos instrumentos:
Instrumento[] banda = [new Violao(), new Piano()];
foreach (Instrumento i in banda)
{
i.Tocar();
}
abstract class Instrumento
{
public abstract string Nome { get; }
public abstract string Som { get; }
public void Tocar()
{
Console.WriteLine($"O {Nome} faz: {Som}");
}
}
class Violao : Instrumento
{
public override string Nome => "violão";
public override string Som => "plim plim";
}
class Piano : Instrumento
{
public override string Nome => "piano";
public override string Som => "plém";
}
- public abstract string Nome { get; }
- Uma propriedade abstrata só de leitura: "todo instrumento tem um nome", mas a mãe não sabe qual.
- public override string Nome => "violão";
- A filha implementa a propriedade. O
=>é a forma curta de umgetque só devolve um valor: o mesmo queget { return "violão"; }. - public void Tocar()
- O método pronto da mãe, que usa as duas propriedades das filhas.
$ dotnet run instrumentos.cs O violão faz: plim plim O piano faz: plém
As regras dos membros abstratos
Membro abstrato não tem corpo
Nem chaves vazias. Um abstract com corpo é uma contradição. A Ana escreveu public abstract double Area() { return 0; }:
$ dotnet run comcorpo.cs /home/ana/cs/comcorpo.cs(16,28): error CS0500: "Forma.Area()" não pode declarar um corpo porque não está marcado como abstract Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
A mensagem em português diz "porque não está marcado como abstract", mas é o contrário! O original em inglês é 'Forma.Area()' cannot declare a body because it is marked abstract: "não pode ter corpo porque está marcado como abstract". O código CS0500 é o que vale. Troque { … } por ;. Se você quer um comportamento padrão que as filhas podem trocar, use virtual no lugar de abstract.
Membro abstrato só em classe abstrata
Se a classe tem um membro abstrato, ela precisa ser abstrata (senão daria para criar um objeto com um método "em branco"). Tirando o abstract da classe Forma:
$ dotnet run naoabstrata.cs /home/ana/cs/naoabstrata.cs(16,28): error CS0513: 'Forma.Area()' é abstrato, mas está contido no tipo não abstrato 'Forma' Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O contrário é permitido: uma classe abstrata sem nenhum membro abstrato. Isso serve para impedir que alguém crie objetos de uma classe que só existe para ser herdada.
Membro abstrato não pode ser private
Uma filha não enxerga o que é private da mãe (veja public, private e internal), então nunca poderia implementá-lo. Com private abstract double Area();, o primeiro erro é:
$ dotnet run privado.cs /home/ana/cs/privado.cs(16,29): error CS0621: "Forma.Area()": membros virtuais ou abstratos não podem ser privados …
Use public ou protected (só a família enxerga).
abstract e sealed não combinam
abstract quer dizer "precisa ser herdada"; sealed ("selada") quer dizer "não pode ser herdada" (veja sealed). Juntos, em abstract sealed class Forma:
$ dotnet run selada.cs /home/ana/cs/selada.cs(7,23): error CS0418: 'Forma': um tipo abstrato não pode ser selado nem estático /home/ana/cs/selada.cs(11,15): warning CS0628: 'Forma.Forma(string)': novo membro protegido declarado no tipo selado /home/ana/cs/selada.cs(24,19): error CS0509: "Retangulo": não é possível derivar do tipo sealed "Forma" /home/ana/cs/selada.cs(35,28): error CS0115: "Retangulo.Area()": não encontrado nenhum método adequado para substituição /home/ana/cs/selada.cs(41,17): error CS0509: "Circulo": não é possível derivar do tipo sealed "Forma" /home/ana/cs/selada.cs(50,28): error CS0115: "Circulo.Area()": não encontrado nenhum método adequado para substituição Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Um erro só (CS0418, o primeiro) gera vários outros: com a mãe selada, as filhas não podem herdar (CS0509) e os override ficam sem nada para substituir (CS0115). Regra de ouro dos erros: conserte o primeiro e compile de novo. Tirando o sealed, todos somem. A mensagem também cita static: uma classe estática não pode ser abstrata pelo mesmo motivo (ninguém herda dela).
Classe abstrata × interface
A próxima aula apresenta as interfaces, que também declaram membros sem corpo e também não podem ser instanciadas (repare que a mensagem do CS0144 já fala em "tipo abstrato ou interface"). Então, quando usar cada uma? A diferença principal:
- Uma classe abstrata é uma "mãe incompleta": tem estado (campos e propriedades como
Nome), construtores e código pronto para compartilhar. Uma classe só pode herdar de uma. - Uma interface é um "contrato de habilidade": diz o que uma classe sabe fazer (
IComparable: "sei me comparar"). Não guarda estado de objeto, e uma classe pode implementar várias.
| Situação | Use |
|---|---|
| As classes são da mesma família ("é um") e compartilham dados e código | Classe abstrata (Forma, Funcionario, Conta) |
| Classes sem parentesco precisam de uma mesma habilidade | Interface (IComparable, IImprimivel, IPagavel) |
| A classe precisa de mais de um "tipo pai" | Interfaces (só há uma classe mãe) |
| Há um construtor ou campos que todas as filhas precisam | Classe abstrata |
Muitas vezes as duas aparecem juntas: uma interface diz o que fazer, e uma classe abstrata implementa a parte comum para facilitar a vida das filhas. A comparação completa está em Interfaces.
Quando dá errado
$ dotnet run instancia.cs /home/ana/cs/instancia.cs(1,11): error CS0144: Não é possível criar uma instância do tipo abstrato ou da interface 'Forma'
O que aconteceu: um new numa classe abstrata.
Como resolver: crie um objeto de uma filha concreta (new Circulo(…)). A variável pode continuar do tipo Forma.
$ dotnet run semarea.cs /home/ana/cs/semarea.cs(41,7): error CS0534: "Circulo" não implementa membro abstrato herdado "Forma.Area()"
O que aconteceu: uma classe concreta herdou um membro abstrato e não o escreveu. Se o método existe, confira: está com override? A assinatura é idêntica (um Area(int x) não conta)? Se houver um aviso CS0114 logo antes, faltou o override.
Como resolver: implemente o membro com public override …, ou declare a classe como abstract se ela também for uma ideia geral.
$ dotnet run comcorpo.cs /home/ana/cs/comcorpo.cs(16,28): error CS0500: "Forma.Area()" não pode declarar um corpo porque não está marcado como abstract
O que aconteceu: o método abstrato ganhou chaves (a tradução troca o sentido; o certo é "porque está marcado como abstract").
Como resolver: se ele é abstrato, termine com ;. Se quer um corpo padrão, troque abstract por virtual.
$ dotnet run naoabstrata.cs /home/ana/cs/naoabstrata.cs(16,28): error CS0513: 'Forma.Area()' é abstrato, mas está contido no tipo não abstrato 'Forma'
O que aconteceu: um membro abstrato numa classe comum.
Como resolver: escreva abstract class ou dê um corpo ao membro (e troque abstract por virtual).
Experimente o erro: apague o override do Piano (a linha do Som) e clique em Rodar. Veja o erro CS0534: no dotnet, ele diz "Piano" não implementa membro abstrato herdado "Instrumento.Som.get". O .get é a parte de leitura da propriedade, que ficou faltando. Depois, coloque a linha de volta e tente new Instrumento() no começo do programa para ver o CS0144.
Instrumento[] banda = [new Violao(), new Piano()];
foreach (Instrumento i in banda)
{
i.Tocar();
}
abstract class Instrumento
{
public abstract string Nome { get; }
public abstract string Som { get; }
public void Tocar()
{
Console.WriteLine($"O {Nome} faz: {Som}");
}
}
class Violao : Instrumento
{
public override string Nome => "violão";
public override string Som => "plim plim";
}
class Piano : Instrumento
{
public override string Nome => "piano";
public override string Som => "plém";
}
Instrumento[] banda = [new Violao(), new Piano()];
foreach (Instrumento i in banda)
{
i.Tocar();
}
abstract class Instrumento
{
public abstract string Nome { get; }
public abstract string Som { get; }
public void Tocar()
{
Console.WriteLine($"O {Nome} faz: {Som}");
}
}
class Violao : Instrumento
{
public override string Nome => "violão";
public override string Som => "plim plim";
}
class Piano : Instrumento
{
public override string Nome => "piano";
}
Referência
| Escrita | Significado |
|---|---|
abstract class Forma { … } | Classe que não pode ser instanciada; serve de modelo |
public abstract double Area(); | Método sem corpo; as filhas concretas são obrigadas a implementar |
public abstract string Nome { get; } | Propriedade abstrata |
public override double Area() { … } | A implementação na filha (o override é obrigatório) |
| Método concreto em classe abstrata | Código pronto, herdado por todas as filhas (pode chamar os abstratos) |
protected Forma(string nome) | Construtor da classe abstrata, chamado pelas filhas com : base(…) |
abstract class Poligono : Forma | Filha abstrata: pode deixar os membros abstratos para as netas |
abstract + sealed/static na classe; abstract + private no membro | Combinações proibidas (CS0418, CS0621) |
Exercícios
1. A classe-modelo
Complete para Instrumento não poder ser instanciado:
2. Método sem corpo
Complete a declaração do método abstrato público Tocar, que não recebe nada e não devolve nada:
3. A implementação na filha
Na classe Violao : Instrumento, complete a primeira linha do método que implementa o Tocar do exercício 2:
4. Variável de tipo abstrato
Instrumento é abstrata e Violao herda de Instrumento. Complete com o objeto que pode ser criado:
5. O que uma classe abstrata não pode ter?
6. Circulo : Forma, Forma tem public abstract double Area(); e o Circulo não escreve Area(). O que acontece?
7. O Circulo tem public double Area(), sem override. O que o compilador mostra?
8. Cachorro, Gato e Vaca compartilham as propriedades Nome e Idade e o método Comer(). O que é mais adequado para Animal?
Missão final: a lojinha da Ana tem funcionários mensalistas (salário fixo) e horistas (horas × valor da hora). Hoje, Funcionario é uma classe comum com Salario() devolvendo 0, e o programa cria um "funcionário genérico" sem sentido. Transforme: (1) Funcionario em classe abstrata; (2) Salario() em método abstrato (sem corpo); (3) apague a linha do generico; (4) implemente Salario() em Horista (Horas * ValorHora). A saída deve ter Caio: R$ 1.600,00 e Bia: R$ 5.000,00. Dica: depois do passo 2, rode e leia o erro: ele diz o que falta.
Funcionario generico = new Funcionario("Ninguém");
List<Funcionario> equipe = [new Horista("Caio", 80, 20m), new Mensalista("Bia", 5000m)];
foreach (Funcionario f in equipe)
{
Console.WriteLine($"{f.Nome}: {f.Salario():C}");
}
class Funcionario
{
public string Nome { get; }
public Funcionario(string nome)
{
Nome = nome;
}
public virtual decimal Salario()
{
return 0;
}
}
class Mensalista : Funcionario
{
public decimal Fixo { get; }
public Mensalista(string nome, decimal fixo) : base(nome)
{
Fixo = fixo;
}
public override decimal Salario() => Fixo;
}
class Horista : Funcionario
{
public int Horas { get; }
public decimal ValorHora { get; }
public Horista(string nome, int horas, decimal valorHora) : base(nome)
{
Horas = horas;
ValorHora = valorHora;
}
}
List<Funcionario> equipe = [new Horista("Caio", 80, 20m), new Mensalista("Bia", 5000m)];
foreach (Funcionario f in equipe)
{
Console.WriteLine($"{f.Nome}: {f.Salario():C}");
}
abstract class Funcionario
{
public string Nome { get; }
protected Funcionario(string nome)
{
Nome = nome;
}
public abstract decimal Salario();
}
class Mensalista : Funcionario
{
public decimal Fixo { get; }
public Mensalista(string nome, decimal fixo) : base(nome)
{
Fixo = fixo;
}
public override decimal Salario() => Fixo;
}
class Horista : Funcionario
{
public int Horas { get; }
public decimal ValorHora { get; }
public Horista(string nome, int horas, decimal valorHora) : base(nome)
{
Horas = horas;
ValorHora = valorHora;
}
public override decimal Salario() => Horas * ValorHora;
}
$ dotnet run func.cs « a solução Caio: R$ 1.600,00 Bia: R$ 5.000,00
Resumo
abstract class= uma ideia geral, que serve de modelo: tem propriedades, construtores e métodos, mas não pode ser instanciada (CS0144).abstractnum método ou propriedade = só a assinatura, terminada em;. Toda filha concreta é obrigada a implementá-lo comoverride(CS0534). Sem ooverride, vem o avisoCS0114e o mesmo erro.- Métodos concretos da classe abstrata podem chamar os abstratos: a mãe escreve o roteiro, as filhas preenchem as partes que mudam.
- O construtor de uma classe abstrata costuma ser
protected; as filhas o chamam com: base(…). - Variáveis, listas e parâmetros do tipo abstrato são o normal:
Forma f = new Circulo(…). - Classe abstrata: família com dados e código em comum (só uma mãe). Interface: habilidade que várias classes, mesmo sem parentesco, podem ter. Próxima aula: Interfaces.