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.
- Que problema o encapsulamento resolve (a conta com saldo negativo).
- O padrão do C#: campo
private+ propriedadepublic. - Como validar no
sete quando é melhor ter métodos comoDepositar. - Propriedades só de leitura,
private sete propriedades calculadas. - A palavra
fieldC# 14, que dispensa o campo escrito à mão. - Como recusar um valor com uma exceção.
- Os erros
CS0200,CS0272e oStack overflowdosetque 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:Saldopara consultar,DepositareSacarpara 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._saldonem 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 nenhumset("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.
Sacarainda devolvetrueoufalse, 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.
| Dado | get? | set? | Por quê |
|---|---|---|---|
Titular | Sim | Sim, com validação (ou nenhum) | O nome pode ser corrigido, mas nunca ficar vazio |
Saldo | Sim | Não: Depositar/Sacar | Muda só por operações com regras |
Numero da conta | Sim | Não | Definido no construtor e nunca muda |
Senha | Não | TrocarSenha(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
setdeNotaé um método chamadoset_Nota. A linha 18 é a dothrow. - 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
| Forma | Como fica | Quando usar |
|---|---|---|
| Campo público | public decimal Saldo; | Só em exemplos rápidos |
| Propriedade automática | { get; set; } | Dados que podem mudar sem regra nenhuma |
Propriedade com validação no set | set { 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 construtor | Dados que nunca mudam (veja readonly e init) |
| Calculada | Media => …; | Valores que saem de outros dados |
Quando dá errado
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.
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.
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 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
| Escrita | Significado |
|---|---|
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# 14 | field = 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(comvalue) ou em métodos de negócio, comoDepositareSacar. - 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. fieldC# 14 dispensa o campo escrito à mão.- Erros:
CS0200(somente leitura),CS0272(setinacessível) e oStack overflow.dosetque chama a si mesmo.