C# Encapsulamento

Encapsular é guardar algo numa cápsula. Os dados de um objeto ficam escondidos lá dentro, e o resto do programa só consegue lê-los ou mudá-los pelas portas que a classe oferece, cada uma com as suas regras. É o primeiro pilar da orientação a objetos. Em C#, as ferramentas para isso são as que você já conhece: campos private, métodos e propriedades.

Você vai aprender
  • Que problema o encapsulamento resolve (a conta com saldo negativo).
  • O padrão do C#: campo private + propriedade public.
  • Como validar no set e quando é melhor ter métodos como Depositar.
  • Propriedades só de leitura, private set e propriedades calculadas.
  • A palavra field C# 14, que dispensa o campo escrito à mão.
  • Como recusar um valor com uma exceção.
  • Os erros CS0200, CS0272 e o Stack overflow do set que chama a si mesmo.

Esta aula junta três anteriores: Propriedades (get e set), Construtores e public, private e internal.


O problema: a conta com saldo negativo

O banco da Ana começou com uma classe Conta de campos abertos, como nas primeiras aulas de classes. Qualquer parte do programa mexe neles diretamente:

var conta = new Conta();
conta.Titular = "Ana";
conta.Saldo = 100m;
conta.Saldo = conta.Saldo - 600m;
conta.Titular = "";
Console.WriteLine($"Titular: [{conta.Titular}]  Saldo: {conta.Saldo:C}");

class Conta
{
    public string Titular = "";
    public decimal Saldo;
}
$ dotnet run aberta.cs
Titular: []  Saldo: -R$ 500,00       « uma conta sem dono e com 500 negativos

O C# não reclamou de nada: compilou e rodou. Mas a conta ficou num estado inválido: sacaram 600 de quem tinha 100, e o titular virou um texto vazio. Num sistema grande, com centenas de lugares mexendo em Saldo, descobrir quem fez isso é um pesadelo.

O defeito não está nas contas: é que a classe não tem como se defender. Qualquer um escreve qualquer valor nos campos dela.


A ideia: o cofre e o balcão

Pense no banco de verdade. Você não entra no cofre: fala com o atendente no balcão. Ele confere se o saque cabe no saldo e só então mexe no cofre. O cofre fica fechado; o balcão é a única passagem, e a passagem tem regras.

  • O cofre são os campos, marcados com private.
  • O balcão são as propriedades e os métodos public: Saldo para consultar, Depositar e Sacar para mudar, cada um com as suas conferências.

A conta encapsulada

var conta = new Conta("Ana");
conta.Depositar(100m);
conta.Sacar(600m);
conta.Depositar(-50m);
conta.Sacar(30m);
Console.WriteLine($"{conta.Titular}: {conta.Saldo:C}");

class Conta
{
    private decimal _saldo;

    public Conta(string titular)
    {
        Titular = titular;
    }

    public string Titular { get; }

    public decimal Saldo
    {
        get { return _saldo; }
    }

    public void Depositar(decimal valor)
    {
        if (valor <= 0)
        {
            Console.WriteLine($"Depósito recusado: {valor:C}");
            return;
        }
        _saldo += valor;
    }

    public bool Sacar(decimal valor)
    {
        if (valor <= 0 || valor > _saldo)
        {
            Console.WriteLine($"Saque recusado: {valor:C}");
            return false;
        }
        _saldo -= valor;
        return true;
    }
}
$ dotnet run conta.cs
Saque recusado: R$ 600,00
Depósito recusado: -R$ 50,00
Ana: R$ 70,00          « a conta continua válida

As mesmas tentativas do exemplo anterior agora são barradas. Peça por peça:

private decimal _saldo;
O cofre. De fora da classe, conta._saldo nem compila (CS0122, da aula anterior).
public Conta(string titular)
O construtor: a conta já nasce com dono.
public string Titular { get; }
Uma propriedade só de leitura: tem get ("pegar") e nenhum set ("definir"). Só o construtor pode dar valor a ela. Depois disso, ninguém troca o titular.
public decimal Saldo { get { return _saldo; } }
A janelinha do cofre: mostra o saldo, mas não deixa escrever nele. Repare: não existe set.
Depositar e Sacar
As únicas formas de mudar o saldo, cada uma com a sua regra. Sacar ainda devolve true ou false, para quem chamou saber se deu certo.

E se alguém tentar escrever no saldo por fora?

var conta = new Conta();
conta.Depositar(100m);
conta.Saldo = 1000000m;

class Conta
{
    private decimal _saldo;

    public decimal Saldo
    {
        get { return _saldo; }
    }

    public void Depositar(decimal valor)
    {
        _saldo += valor;
    }
}
$ dotnet run conta-erro.cs
/home/ana/cs/conta-erro.cs(3,1): error CS0200: A propriedade ou o indexador "Conta.Saldo" não pode ser atribuído, pois é somente leitura

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

"Somente leitura": a propriedade não tem set. Em inglês, Property or indexer 'Conta.Saldo' cannot be assigned to -- it is read only.

Nem todo dado precisa de set

Um erro comum é criar, por reflexo, { get; set; } em tudo. Um Saldo { get; set; } público deixaria qualquer um escrever conta.Saldo = 1000000m: o campo estaria escondido só no nome. Pergunte sempre: como esse dado muda no mundo real? O saldo muda por depósito e saque, então a classe oferece Depositar e Sacar. O número da conta nunca muda, então não tem set nenhum.

Dadoget?set?Por quê
TitularSimSim, com validação (ou nenhum)O nome pode ser corrigido, mas nunca ficar vazio
SaldoSimNão: Depositar/SacarMuda só por operações com regras
Numero da contaSimNãoDefinido no construtor e nunca muda
SenhaNãoTrocarSenha(antiga, nova)Ninguém de fora precisa ler a senha

Validar no set

Quando um dado pode mudar livremente, desde que seja válido, o lugar da regra é o set da propriedade. Dentro do set, a palavra value ("valor") é o que está chegando: em aluno.Nota = 12m;, value vale 12.

Missão: complete o set da propriedade Nota: ele só deve aceitar notas de 0 a 10. Para valores fora disso, mostre Nota recusada: seguido do valor e não mude nada. O programa tenta 12, -1 e 9,5.

var aluno = new Aluno("Carla");
aluno.Nota = 12m;
aluno.Nota = -1m;
aluno.Nota = 9.5m;
Console.WriteLine($"{aluno.Nome}: {aluno.Nota}");

class Aluno
{
    private decimal _nota;

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

    public string Nome { get; }

    public decimal Nota
    {
        get { return _nota; }
        set
        {
            // valide aqui
            _nota = value;
        }
    }
}
var aluno = new Aluno("Carla");
aluno.Nota = 12m;
aluno.Nota = -1m;
aluno.Nota = 9.5m;
Console.WriteLine($"{aluno.Nome}: {aluno.Nota}");

class Aluno
{
    private decimal _nota;

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

    public string Nome { get; }

    public decimal Nota
    {
        get { return _nota; }
        set
        {
            if (value < 0 || value > 10)
            {
                Console.WriteLine($"Nota recusada: {value}");
                return;
            }
            _nota = value;
        }
    }
}

Quem usa a classe continua escrevendo aluno.Nota = 9.5m;, como se fosse um campo. A diferença é que agora existe um porteiro no meio do caminho. Essa é a grande vantagem das propriedades do C# sobre os métodos getNota()/setNota() de outras linguagens: a regra fica escondida, e a escrita continua simples.


Recusar com aviso ou com exceção?

Até aqui, o set mostra um aviso e ignora o valor. É simples e bom para aprender, mas tem um defeito: quem chamou pode não perceber que nada aconteceu. Em programas reais, o costume é lançar uma exceção: o programa para (a menos que alguém trate o problema) e a mensagem diz exatamente o que houve.

var aluno = new Aluno();
aluno.Nota = 8.5m;
Console.WriteLine($"Nota: {aluno.Nota}");
aluno.Nota = 12m;
Console.WriteLine("Esta linha não roda");

class Aluno
{
    private decimal _nota;

    public decimal Nota
    {
        get { return _nota; }
        set
        {
            if (value < 0 || value > 10)
            {
                throw new ArgumentOutOfRangeException(nameof(value), $"A nota tem de ser de 0 a 10, e veio {value}.");
            }
            _nota = value;
        }
    }
}
$ dotnet run excecao.cs
Nota: 8,5
Unhandled exception. System.ArgumentOutOfRangeException: A nota tem de ser de 0 a 10, e veio 12. (Parameter 'value')
   at Aluno.set_Nota(Decimal value) in /home/ana/cs/excecao.cs:line 18
   at Program.<Main>$(String[] args) in /home/ana/cs/excecao.cs:line 4
$ echo $?
134                 « código de saída de um programa que parou por exceção
throw new ArgumentOutOfRangeException(…)
throw = "lançar". ArgumentOutOfRangeException = "argumento fora do intervalo": a exceção certa para "o valor não está entre o mínimo e o máximo".
nameof(value)
O nome do parâmetro que veio errado, como texto. Aparece no fim da mensagem: (Parameter 'value').
at Aluno.set_Nota(Decimal value) … line 18
Por baixo dos panos, o set de Nota é um método chamado set_Nota. A linha 18 é a do throw.
at Program.<Main>$ … line 4
Quem chamou: a linha 4, aluno.Nota = 12m;.

As mensagens das exceções saem em inglês, mesmo num Linux em português. Lançar e tratar exceções é assunto de throw e try e catch. Por enquanto, saiba que é a forma profissional de um set dizer "não".


private set: lê-se de fora, muda-se por dentro

Escrever o campo _saldo e a propriedade Saldo separados dá trabalho. Quando a regra está nos métodos (e não no set), dá para usar uma propriedade automática com o set fechado: { get; private set; }. O get é público; o set, só a classe usa.

var conta = new Conta();
conta.Depositar(100m);
Console.WriteLine(conta.Saldo);
conta.Saldo = 1000000m;

class Conta
{
    public decimal Saldo { get; private set; }

    public void Depositar(decimal valor)
    {
        if (valor > 0)
        {
            Saldo += valor;
        }
    }
}
$ dotnet run privset.cs
/home/ana/cs/privset.cs(4,1): error CS0272: A propriedade ou o indexador "Conta.Saldo" não pode ser usado neste contexto porque o acessador set é inacessível

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

Dentro de Depositar, Saldo += valor; funciona. Na linha 4, fora da classe, o set é inacessível. É o mesmo efeito da conta encapsulada, com menos código. Apagando a linha 4, o programa mostra 100.


Propriedades calculadas

Uma propriedade não precisa guardar nada: ela pode calcular o valor na hora em que alguém pede. A média de um aluno, por exemplo, sai das notas:

var bia = new Aluno("Bia", 8m, 7m);
Console.WriteLine($"{bia.Nome}: média {bia.Media}, {bia.Situacao}");

class Aluno
{
    private decimal _nota1;
    private decimal _nota2;

    public Aluno(string nome, decimal nota1, decimal nota2)
    {
        Nome = nome;
        _nota1 = nota1;
        _nota2 = nota2;
    }

    public string Nome { get; }
    public decimal Media => (_nota1 + _nota2) / 2;
    public string Situacao => Media >= 7 ? "aprovada" : "em recuperação";
}
$ dotnet run media.cs
Bia: média 7,5, aprovada

O => depois do nome é um jeito curto de escrever { get { return …; } }. A grande vantagem aparece com o tempo: se amanhã a Ana decidir guardar as notas numa lista em vez de _nota1 e _nota2, só o interior da classe muda. Quem usa bia.Media nem fica sabendo. É isso que "esconder os detalhes" compra: liberdade para mudar por dentro sem quebrar quem está de fora.


A palavra field C# 14

Às vezes você quer uma regra pequena no set (tirar os espaços, por exemplo), mas não quer escrever o campo _nome à mão. O C# 14 trouxe a palavra field ("campo"): dentro de um get ou set, ela é o campo escondido que o compilador cria sozinho.

var p = new Produto();
p.Nome = "   Caderno  ";
Console.WriteLine($"[{p.Nome}]");

class Produto
{
    public string Nome
    {
        get;
        set => field = value.Trim();
    } = "";
}
$ dotnet run field.cs
[Caderno]

O get; sozinho devolve o campo; o set guarda nele o valor sem os espaços (Trim, veja Métodos de String). O = ""; no fim dá o valor inicial. Antes do C# 14, era preciso declarar private string _nome = ""; e usar _nome no lugar de field. Você vai ver muito código assim, e os dois jeitos dão o mesmo resultado.


Formas diferentes de proteger os dados

FormaComo ficaQuando usar
Campo públicopublic decimal Saldo;Só em exemplos rápidos
Propriedade automática{ get; set; }Dados que podem mudar sem regra nenhuma
Propriedade com validação no setset { if (…) throw …; _nota = value; }Dados que mudam livremente, desde que válidos
private set + métodos{ get; private set; }, Depositar(…)Dados que mudam só por operações com regras
Só get{ get; }, preenchida no construtorDados que nunca mudam (veja readonly e init)
CalculadaMedia => …;Valores que saem de outros dados

Quando dá errado

error CS0200: A propriedade ou o indexador "…" não pode ser atribuído, pois é somente leitura

O que aconteceu: o código tentou escrever numa propriedade que só tem get.

Como resolver: use o método que a classe oferece para aquilo (conta.Depositar(…)). Se nenhum método faz o que você precisa, pergunte se a classe deveria permitir antes de criar um set.

error CS0272: … não pode ser usado neste contexto porque o acessador set é inacessível

O que aconteceu: a propriedade tem private set, e você escreveu nela de fora da classe.

Como resolver: o mesmo do caso anterior: mude o valor pelos métodos da classe.

Stack overflow. (o set que chama a si mesmo)

Este bug compila sem erro nem aviso e derruba o programa:

var p = new Pessoa();
p.Nome = "Ana";
Console.WriteLine(p.Nome);

class Pessoa
{
    public string Nome
    {
        get { return Nome; }
        set { Nome = value; }
    }
}
$ dotnet run recursao.cs
Stack overflow.
Repeated 261732 times:          « o número muda de uma máquina para outra
--------------------------------
   at Pessoa.set_Nome(System.String)
--------------------------------
   at Program.<Main>$(System.String[])
$ echo $?
134

O que aconteceu: dentro do set, Nome = value; não guarda nada: chama o set de Nome de novo, que chama de novo, centenas de milhares de vezes, até a memória das chamadas (a pilha, "stack") transbordar (overflow). Veja Recursão.

Como resolver: guarde num campo de verdade: private string _nome = ""; com get { return _nome; } e set { _nome = value; }. Ou use { get; set; }, ou field.

O set "não funciona" (o valor não muda)

O que aconteceu: quase sempre a validação recusou o valor em silêncio, ou o set guarda num campo diferente do que o get lê.

Como resolver: confira que get e set usam o mesmo campo. E faça o set avisar (mensagem ou exceção) quando recusar algo.


Referência

EscritaSignificado
private decimal _saldo;Campo escondido; só a classe mexe
public decimal Saldo { get { return _saldo; } }Propriedade só de leitura sobre um campo
public string Titular { get; }Propriedade só de leitura; valor dado no construtor
public decimal Saldo { get; private set; }Lê-se de fora, muda-se só por dentro
set { if (…) { … return; } _nota = value; }Validação no set; value é o valor que chega
throw new ArgumentOutOfRangeException(nameof(value), "…");Recusa um valor fora do intervalo, interrompendo o programa
public decimal Media => …;Propriedade calculada
set => field = value.Trim(); C# 14field = o campo escondido criado pelo compilador

Missão final: o placar do basquete

Missão: no basquete, uma cesta vale 1, 2 ou 3 pontos. Hoje qualquer um muda o Pontos do Placar, e ele aceita qualquer jogada. Encapsule: troque o campo por uma propriedade public int Pontos { get; private set; } e faça o Marcar recusar valores fora de 1 a 3, mostrando Jogada inválida: e o valor.

var placar = new Placar();
placar.Marcar(2);
placar.Marcar(3);
placar.Marcar(5);
placar.Marcar(1);
Console.WriteLine($"Pontos: {placar.Pontos}");

class Placar
{
    public int Pontos;

    public void Marcar(int pontos)
    {
        Pontos += pontos;
    }
}
var placar = new Placar();
placar.Marcar(2);
placar.Marcar(3);
placar.Marcar(5);
placar.Marcar(1);
Console.WriteLine($"Pontos: {placar.Pontos}");

class Placar
{
    public int Pontos { get; private set; }

    public void Marcar(int pontos)
    {
        if (pontos < 1 || pontos > 3)
        {
            Console.WriteLine($"Jogada inválida: {pontos}");
            return;
        }
        Pontos += pontos;
    }
}

Exercícios

1. O valor que chega

Dentro de um set, qual palavra representa o valor que está sendo atribuído?

2. Lê de fora, muda por dentro

Complete a propriedade para que todos leiam, mas só a classe mude: public int Vidas { get; ___ }

3. O código do "somente leitura"

Qual é o código do erro ao atribuir a uma propriedade que só tem get? (como CS0000)

4. Qual é a melhor forma de mudar o saldo de uma conta encapsulada?

5. O que acontece com public string Nome { get { return Nome; } set { Nome = value; } }?

6. No C# 14, o que é field dentro de set => field = value.Trim();?


Resumo

  • Encapsular é esconder os campos (private) e expor só propriedades e métodos com regras (public). O objeto passa a se defender de valores inválidos.
  • Validação mora no set (com value) ou em métodos de negócio, como Depositar e Sacar.
  • Nem todo dado precisa de set: { get; } (nunca muda), { get; private set; } (muda só por dentro), => (calculado).
  • Para recusar, um aviso é simples; lançar ArgumentOutOfRangeException é o jeito profissional.
  • field C# 14 dispensa o campo escrito à mão.
  • Erros: CS0200 (somente leitura), CS0272 (set inacessível) e o Stack overflow. do set que chama a si mesmo.