C# sealed
A herança deixa qualquer classe virar mãe de outra, e o override deixa as filhas trocarem os métodos da mãe. Na maior parte do tempo, isso é ótimo. Mas às vezes você não quer que ninguém mexa: uma conta bancária cujas regras não podem ser burladas, um cálculo de imposto que tem de ser sempre o mesmo. A palavra sealed ("selado", como um envelope lacrado) fecha a porta: uma classe sealed não pode ter filhas, e um método sealed override não pode mais ser trocado.
- O que é uma classe selada (
sealed class) e o erro de quem tenta herdar dela (CS0509). - Por que o
stringdo .NET é selado. - Como selar só um método:
sealed override(e os errosCS0239eCS0238). - Por que
protectednuma classe selada dá aviso (CS0628). - Quando vale a pena selar.
Todos os programas desta aula rodaram com dotnet run arquivo.cs no .NET 10 da máquina da Ana, na pasta ~/cs.
A ideia: o envelope lacrado
Quando um documento importante vai pelo correio, ele vai num envelope lacrado: quem recebe sabe que ninguém abriu e mudou o conteúdo no caminho. Uma classe selada é assim. Quem usa a classe Conta sabe que todo objeto Conta é exatamente aquela classe, com aquelas regras, e não uma filha "esperta" que trocou o comportamento.
Se você já viu Java: o sealed do C# é o final do Java. (O Java também tem uma palavra sealed, mas com outro significado: lá ela lista quem pode herdar.)
Uma classe selada
Conta c = new Conta("Ana", 100m);
c.Depositar(50m);
Console.WriteLine($"{c.Titular}: {c.Saldo:C}");
sealed class Conta
{
public string Titular { get; }
public decimal Saldo { get; private set; }
public Conta(string titular, decimal saldo)
{
Titular = titular;
Saldo = saldo;
}
public void Depositar(decimal valor)
{
Saldo += valor;
}
}
- sealed class Conta
- Uma classe selada. Por dentro ela é igual a qualquer classe: propriedades, construtor, métodos. A única diferença é que nenhuma classe pode herdar dela.
- public decimal Saldo { get; private set; }
- O saldo pode ser lido por todos, mas só a própria classe muda o valor (veja Encapsulamento).
- Conta c = new Conta("Ana", 100m);
- Criar objetos de uma classe selada é normal. Selar não impede o uso; impede a herança.
$ dotnet run selada.cs Ana: R$ 150,00
Agora, alguém tenta criar uma conta "trapaceira", que herda de Conta. A Ana acrescentou no fim do arquivo:
// … tudo o que estava em selada.cs
class ContaTrapaceira : Conta
{
public ContaTrapaceira(string titular) : base(titular, 0m) { }
}
$ dotnet run herdar.cs /home/ana/cs/herdar.cs(22,25): error CS0509: "ContaTrapaceira": não é possível derivar do tipo sealed "Conta" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
"Derivar" é outra palavra para herdar: uma classe filha é uma classe derivada. A coluna 25 da linha 22 aponta o Conta depois do :. Em inglês: 'ContaTrapaceira': cannot derive from sealed type 'Conta'.
Missão: o programa abaixo compila, e a ContaTrapaceira consegue herdar de Conta. Sele a classe Conta (acrescente sealed antes de class Conta) e rode: o compilador tem de recusar a filha com o erro CS0509.
Conta c = new Conta("Ana", 100m);
c.Depositar(50m);
Console.WriteLine($"{c.Titular}: {c.Saldo:C}");
class Conta
{
public string Titular { get; }
public decimal Saldo { get; private set; }
public Conta(string titular, decimal saldo)
{
Titular = titular;
Saldo = saldo;
}
public void Depositar(decimal valor)
{
Saldo += valor;
}
}
class ContaTrapaceira : Conta
{
public ContaTrapaceira(string titular) : base(titular, 0m) { }
}
Conta c = new Conta("Ana", 100m);
c.Depositar(50m);
Console.WriteLine($"{c.Titular}: {c.Saldo:C}");
sealed class Conta
{
public string Titular { get; }
public decimal Saldo { get; private set; }
public Conta(string titular, decimal saldo)
{
Titular = titular;
Saldo = saldo;
}
public void Depositar(decimal valor)
{
Saldo += valor;
}
}
class ContaTrapaceira : Conta
{
public ContaTrapaceira(string titular) : base(titular, 0m) { }
}
O string é selado
Muitas classes do próprio .NET são seladas. A mais famosa é string. Tente criar o seu "texto melhorado":
class MeuTexto : string
{
}
$ dotnet run texto.cs /home/ana/cs/texto.cs(1,18): error CS0509: "MeuTexto": não é possível derivar do tipo sealed "string" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Por quê? Todo o .NET conta com o fato de que um string é imutável (nunca muda depois de criado; veja Strings) e se compara pelo conteúdo. Uma filha poderia quebrar essas promessas e, com elas, dicionários, senhas, nomes de arquivo... Selando, os criadores do .NET garantem que todo string é o string deles. Para acrescentar métodos a um tipo selado, o C# tem outro caminho: os métodos de extensão.
Dá para perguntar ao próprio .NET se um tipo é selado, com typeof(…).IsSealed (você verá o typeof em Reflexão):
Console.WriteLine(typeof(string).IsSealed); Console.WriteLine(typeof(System.Text.StringBuilder).IsSealed); Console.WriteLine(typeof(Exception).IsSealed);
$ dotnet run lista.cs True True False « Exception foi feita para ser herdada (veja Exceções Próprias)
Selar só um método: sealed override
Às vezes a classe pode ter filhas, mas um método específico já está "no ponto" e não deve mais mudar dali para baixo. Para isso, o sealed vai no método, junto do override:
Forma[] formas = [new Forma(), new Quadrado(), new QuadradoColorido()];
foreach (Forma f in formas)
{
Console.WriteLine(f.Descrever());
}
class Forma
{
public virtual string Descrever() => "Uma forma qualquer";
}
class Quadrado : Forma
{
public sealed override string Descrever() => "Um quadrado: 4 lados iguais";
}
class QuadradoColorido : Quadrado
{
public string Cor => "azul";
}
- public virtual string Descrever()
- Na mãe, o método é
virtual: as filhas podem trocar (veja virtual e override). - public sealed override string Descrever()
- O
Quadradotroca o método (override) e lacra a sua versão (sealed). Daqui para baixo, ninguém troca mais. - class QuadradoColorido : Quadrado
- A classe
Quadradonão é selada, então pode ter filhas. OQuadradoColoridoherda tudo e acrescenta aCor, mas fica com oDescrever()do quadrado.
$ dotnet run metodo.cs Uma forma qualquer Um quadrado: 4 lados iguais Um quadrado: 4 lados iguais « o QuadradoColorido usa a versão lacrada
Se o QuadradoColorido tentar escrever o próprio public override string Descrever():
$ dotnet run metodo-erro.cs /home/ana/cs/metodo-erro.cs(21,28): error CS0239: "QuadradoColorido.Descrever()": não é possível substituir o membro herdado "Quadrado.Descrever()" porque ele é sealed Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
"Substituir" é a tradução de override. O compilador diz qual versão está lacrada: a do Quadrado.
sealed só num override
Um método comum, que não é virtual, já não pode ser trocado: não faz sentido selá-lo. Por isso, sealed num método só vale junto de override. Se a Ana escrever public sealed string Descrever() na Forma:
$ dotnet run naooverride.cs /home/ana/cs/naooverride.cs(9,26): error CS0238: "Forma.Descrever()" não pode ser sealed porque não é uma substituição /home/ana/cs/naooverride.cs(14,35): error CS0506: "Quadrado.Descrever()": não é possível substituir o membro herdado "Forma.Descrever()" porque ele não está marcado como virtual, abstract ou override Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O primeiro erro é o que importa: "não pode ser sealed porque não é uma substituição" (cannot be sealed because it is not an override). O segundo é consequência: sem o virtual, o Quadrado não tem o que substituir. Para "proibir a troca" na mãe, basta não escrever virtual.
| Na classe | As filhas podem trocar o método? |
|---|---|
public string Descrever() | Não (o padrão do C#) |
public virtual string Descrever() | Sim |
public override string Descrever() | Sim, as netas também |
public sealed override string Descrever() | Não, daqui para baixo |
protected numa classe selada
Um membro protected é visível para a própria classe e para as filhas (veja public, private e internal). Numa classe selada não há filhas, então protected é o mesmo que private. O compilador avisa, mas o programa roda:
Console.WriteLine(new Cofre().Abrir());
sealed class Cofre
{
protected string segredo = "1234";
public string Abrir() => $"Aberto com {segredo.Length} dígitos";
}
$ dotnet run protegido.cs /home/ana/cs/protegido.cs(5,22): warning CS0628: 'Cofre.segredo': novo membro protegido declarado no tipo selado Aberto com 4 dígitos
O conserto é trocar protected por private, que diz a verdade sobre quem enxerga o campo.
sealed, abstract e static
abstract sealedé proibido (CS0418): uma classe abstrata existe para ser herdada, e uma selada proíbe a herança. O erro aparece em Classes Abstratas.- Uma
static classjá é fechada (veja static): ninguém herda dela. Tentarclass Mais : Util, comUtilestática, dá outro erro:CS0709: "Mais": não pode derivar da classe static "Util".
Quando selar?
Pense em duas pessoas: quem escreve a classe e quem a usa. Quem permite herança faz uma promessa difícil: "qualquer filha que trocar estes métodos vai funcionar". Muitas vezes a classe não foi pensada para isso.
| Situação | O que fazer |
|---|---|
| A classe não foi pensada para ter filhas (a maioria das classes de um programa) | Pode selar: deixa a intenção clara e evita surpresas |
| Regras que não podem ser burladas (conta, senha, cálculo de imposto) | Selar |
Uma família de classes (Forma, Funcionario) | Não selar a mãe; talvez selar as "folhas" (Circulo, Quadrado) |
| Uma classe herdou um método e a versão dela já é a definitiva | sealed override só naquele método |
| Uma biblioteca usada por outras pessoas | Selar o que não é feito para herança: tirar o sealed depois é fácil; colocar depois quebra quem já herdou |
Quando o tipo é selado, o .NET sabe que um método nunca será trocado por uma filha e pode chamá-lo de forma mais direta. O ganho é pequeno, e não é o motivo principal para selar; o motivo principal é deixar o projeto claro.
Quando dá errado
$ dotnet run herdar.cs /home/ana/cs/herdar.cs(22,25): error CS0509: "ContaTrapaceira": não é possível derivar do tipo sealed "Conta"
O que aconteceu: uma classe tentou herdar de uma classe selada.
Como resolver: se a classe é sua e deve ter filhas, tire o sealed. Se não é sua (como string), use a classe dentro da sua, como um campo ou uma propriedade (composição), ou crie métodos de extensão.
$ dotnet run metodo-erro.cs /home/ana/cs/metodo-erro.cs(21,28): error CS0239: "QuadradoColorido.Descrever()": não é possível substituir o membro herdado "Quadrado.Descrever()" porque ele é sealed
O que aconteceu: uma classe tentou trocar um método lacrado com sealed override numa classe acima dela.
Como resolver: use o método herdado como está, ou, se o método realmente deve mudar, tire o sealed lá na classe que o lacrou.
$ dotnet run naooverride.cs /home/ana/cs/naooverride.cs(9,26): error CS0238: "Forma.Descrever()" não pode ser sealed porque não é uma substituição
O que aconteceu: sealed num método sem override.
Como resolver: apague o sealed: um método sem virtual já não pode ser trocado.
Referência
| Escrita | Significado |
|---|---|
sealed class Conta { … } | Classe que ninguém pode herdar |
public sealed override string Descrever() | Troca o método da mãe e proíbe as filhas de trocá-lo de novo |
protected numa classe selada | Aviso CS0628; use private |
abstract sealed class | Proibido (CS0418) |
typeof(string).IsSealed | True: o string é selado |
| Equivalente em Java | final class e final no método |
Exercícios
1. Lacrar a classe
Complete para que nenhuma classe possa herdar de Senha:
2. Lacrar só o método
Na classe Quadrado : Forma, complete para trocar o Descrever() da mãe e proibir as filhas de trocá-lo de novo:
3. O código do erro
Qual é o código do erro de quem tenta herdar de string?
4. A classe Conta é sealed. O que pode ser feito com ela?
5. Onde o sealed pode ir num método?
6. Por que protected numa classe selada dá o aviso CS0628?
Missão final: a lojinha da Ana aceita Pix e cartão. Faça duas mudanças, sem mudar a saída: (1) sele a classe Pix, porque ninguém deve criar um "Pix especial" com outra taxa; (2) no CartaoInternacional, transforme o override da Taxa() em sealed override. O programa tem de compilar e mostrar Cartão internacional: taxa R$ 9,00.
Pagamento[] pagamentos = [new Pix(100m), new Cartao(100m), new CartaoInternacional(100m)];
foreach (Pagamento p in pagamentos)
{
Console.WriteLine($"{p.Nome}: taxa {p.Taxa():C}");
}
abstract class Pagamento
{
public decimal Valor { get; }
protected Pagamento(decimal valor)
{
Valor = valor;
}
public abstract string Nome { get; }
public abstract decimal Taxa();
}
class Pix : Pagamento
{
public Pix(decimal valor) : base(valor) { }
public override string Nome => "Pix";
public override decimal Taxa() => 0m;
}
class Cartao : Pagamento
{
public Cartao(decimal valor) : base(valor) { }
public override string Nome => "Cartão";
public override decimal Taxa() => Valor * 0.03m;
}
class CartaoInternacional : Cartao
{
public CartaoInternacional(decimal valor) : base(valor) { }
public override string Nome => "Cartão internacional";
public override decimal Taxa() => base.Taxa() + Valor * 0.06m;
}
Pagamento[] pagamentos = [new Pix(100m), new Cartao(100m), new CartaoInternacional(100m)];
foreach (Pagamento p in pagamentos)
{
Console.WriteLine($"{p.Nome}: taxa {p.Taxa():C}");
}
abstract class Pagamento
{
public decimal Valor { get; }
protected Pagamento(decimal valor)
{
Valor = valor;
}
public abstract string Nome { get; }
public abstract decimal Taxa();
}
sealed class Pix : Pagamento
{
public Pix(decimal valor) : base(valor) { }
public override string Nome => "Pix";
public override decimal Taxa() => 0m;
}
class Cartao : Pagamento
{
public Cartao(decimal valor) : base(valor) { }
public override string Nome => "Cartão";
public override decimal Taxa() => Valor * 0.03m;
}
class CartaoInternacional : Cartao
{
public CartaoInternacional(decimal valor) : base(valor) { }
public override string Nome => "Cartão internacional";
public sealed override decimal Taxa() => base.Taxa() + Valor * 0.06m;
}
$ dotnet run pagamentos.cs « a solução Pix: taxa R$ 0,00 Cartão: taxa R$ 3,00 Cartão internacional: taxa R$ 9,00
Repare na linha base.Taxa() + Valor * 0.06m: o cartão internacional usa a taxa do cartão comum (3%) e soma 6%. Depois do sealed override, nenhuma filha do CartaoInternacional pode trocar essa conta.
Resumo
sealed class= classe lacrada: dá para criar objetos, mas não dá para herdar (CS0509). É ofinaldo Java.stringeStringBuildersão selados; para acrescentar métodos a eles, use métodos de extensão.sealed overridelacra só um método: as filhas herdam a versão, mas não podem trocá-la (CS0239).sealedsemoverrideé erro (CS0238).protectednuma classe selada éprivatedisfarçado (avisoCS0628).abstract sealedé proibido (CS0418).- Sele o que não foi pensado para herança e as regras que não podem ser burladas. Próxima aula: Classes Parciais.