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.

Você vai aprender
  • O que é uma classe selada (sealed class) e o erro de quem tenta herdar dela (CS0509).
  • Por que o string do .NET é selado.
  • Como selar só um método: sealed override (e os erros CS0239 e CS0238).
  • Por que protected numa 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:

Este código tem um erro de propósito
// … 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 Quadrado troca o método (override) e lacra a sua versão (sealed). Daqui para baixo, ninguém troca mais.
class QuadradoColorido : Quadrado
A classe Quadrado não é selada, então pode ter filhas. O QuadradoColorido herda tudo e acrescenta a Cor, mas fica com o Descrever() 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 classeAs 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 class já é fechada (veja static): ninguém herda dela. Tentar class Mais : Util, com Util está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çãoO 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 definitivasealed override só naquele método
Uma biblioteca usada por outras pessoasSelar o que não é feito para herança: tirar o sealed depois é fácil; colocar depois quebra quem já herdou
Um bônus: velocidade

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

CS0509: "não é possível derivar do tipo sealed …"
$ 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.

CS0239: "não é possível substituir o membro herdado … porque ele é sealed"
$ 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.

CS0238: "… não pode ser sealed porque não é uma substituição"
$ 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

EscritaSignificado
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 seladaAviso CS0628; use private
abstract sealed classProibido (CS0418)
typeof(string).IsSealedTrue: o string é selado
Equivalente em Javafinal class e final no método

Exercícios

1. Lacrar a classe

Complete para que nenhuma classe possa herdar de Senha:

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:

public string Descrever() => "Quadrado";

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). É o final do Java.
  • string e StringBuilder são selados; para acrescentar métodos a eles, use métodos de extensão.
  • sealed override lacra só um método: as filhas herdam a versão, mas não podem trocá-la (CS0239). sealed sem override é erro (CS0238).
  • protected numa classe selada é private disfarçado (aviso CS0628). 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.