C# Propriedades (get e set)
Na aula Campos, um campo public deixava qualquer linha do programa pôr qualquer valor nele, até um preço negativo. Uma propriedade resolve isso: por fora, ela se usa igualzinho a um campo (p.Preco = 7.90m;); por dentro, cada leitura passa por um get e cada gravação passa por um set, onde a classe pode conferir o valor. Propriedades estão em todo código C#: texto.Length, lista.Count e nota.HasValue são propriedades.
- Por que campos
publicsão perigosos e como uma propriedade os protege. - A anatomia de uma propriedade:
get,set, a palavravaluee o campo de apoio. - As auto-propriedades:
public string Nome { get; set; } = "";. - Propriedades só de leitura:
{ get; },{ get; private set; }e as calculadas, com=>. - A palavra-chave
fielddo C# 14: validar sem escrever o campo de apoio. - Os erros reais:
CS0200,CS0272,CS8618,CS9260e oStack overflowde uma propriedade que chama a si mesma.
Todos os programas desta aula rodaram com dotnet run arquivo.cs na máquina fictícia da Ana (usuária ana, computador notebook), com o .NET 10 SDK (C# 14), na pasta ~/cs.
A ideia antes do código: o balcão da farmácia
Numa farmácia, você não entra no estoque e pega o remédio da prateleira. Você pede no balcão, e o farmacêutico confere a receita antes de entregar. Para quem está do lado de fora, parece simples: "me dá esse remédio". Por dentro, há uma regra sendo cumprida.
A propriedade é o balcão. O campo privado é a prateleira. O get é o farmacêutico entregando; o set é o farmacêutico recebendo uma reposição e conferindo se ela está certa.
O problema: o campo aceita qualquer coisa
Produto p = new Produto();
p.Nome = "maçã";
p.Preco = -5m;
Console.WriteLine($"{p.Nome}: {p.Preco:C}");
class Produto
{
public string Nome = "";
public decimal Preco;
}
$ dotnet run p1.cs maçã: -R$ 5,00
Uma maçã com preço negativo: a lojinha pagaria o cliente para levar a fruta. Nada impediu, porque um campo public é uma prateleira aberta. A Ana precisa de um lugar para a regra "preço não pode ser negativo".
Uma propriedade completa: get e set
Produto p = new Produto();
p.Preco = 7.90m;
Console.WriteLine($"Preço: {p.Preco:C}");
p.Preco = -5m;
Console.WriteLine($"Preço: {p.Preco:C}");
class Produto
{
private decimal _preco;
public decimal Preco
{
get
{
return _preco;
}
set
{
if (value < 0)
{
Console.WriteLine($"Preço inválido: {value}. Mantido {_preco:C}.");
return;
}
_preco = value;
}
}
}
$ dotnet run p2.cs Preço: R$ 7,90 Preço inválido: -5. Mantido R$ 7,90. « o set recusou o -5 Preço: R$ 7,90
- private decimal _preco;
- O campo de apoio (backing field): a prateleira, onde o valor fica guardado de verdade. É
private, então só a classe mexe nele. - public decimal Preco { … }
- A propriedade. Parece a declaração de um campo, mas, em vez de
;, tem um bloco comgeteset. Sem parênteses: não é um método. O nome é o do campo sem o_, em PascalCase. - get { return _preco; }
- "Pegar". Roda toda vez que alguém lê
p.Preco(como no$"{p.Preco:C}"). Tem de devolver um valor do tipo da propriedade. - set { … }
- "Definir". Roda toda vez que alguém grava
p.Preco = …. - value
- Dentro do
set,value("valor") é o que veio do lado direito do=. Emp.Preco = -5m;,valuevale-5. Você não declaravalue: ele já existe em todoset. - _preco = value;
- Só chega aqui se o valor passou na conferência. Aí, sim, ele vai para a prateleira.
Repare nas linhas 2 e 4: por fora, nada mudou. p.Preco = 7.90m; continua parecendo uma atribuição a um campo. Quem usa a classe nem precisa saber que existe uma regra ali dentro. Essa é a ideia do encapsulamento.
Neste exemplo, o set mostra uma mensagem e mantém o preço antigo, para você ver o que aconteceu. Em programas de verdade, o mais comum é lançar uma exceção (throw), que interrompe quem tentou gravar o valor errado. Você verá um exemplo na seção do field, e a explicação completa em throw.
Com =>, mais curto
Quando um get ou set tem uma linha só, dá para usar a flecha => (lê-se "vai para"), sem chaves e sem return:
private string _nome = "";
public string Nome
{
get => _nome;
set => _nome = value.Trim();
}
Este set tira os espaços do começo e do fim de todo nome gravado (veja Métodos de String). É a mesma propriedade escrita com get { return _nome; } e set { _nome = value.Trim(); }.
Missão: o Preco desta classe ainda é um campo, e o preço termina negativo. Transforme-o numa propriedade com um campo de apoio private decimal _preco;, cujo set só grave o valor se ele for maior ou igual a 0. A saída deve ser Preço: R$ 7,90. Não mude as instruções do topo.
Produto p = new Produto();
p.Preco = 7.90m;
p.Preco = -5m;
Console.WriteLine($"Preço: {p.Preco:C}");
class Produto
{
public decimal Preco;
}
Produto p = new Produto();
p.Preco = 7.90m;
p.Preco = -5m;
Console.WriteLine($"Preço: {p.Preco:C}");
class Produto
{
private decimal _preco;
public decimal Preco
{
get => _preco;
set
{
if (value >= 0)
_preco = value;
}
}
}
Auto-propriedades: { get; set; }
Muitas propriedades não têm regra nenhuma: o get só devolve o campo, e o set só grava. Para esses casos, o C# escreve o campo de apoio por você. É a auto-propriedade, a forma mais comum de todas:
Aluno a = new Aluno();
a.Nome = "Bia";
a.Nota1 = 8.0;
a.Nota2 = 9.0;
Console.WriteLine($"{a.Nome}: média {a.Media}");
a.Nota2 = 6.0;
Console.WriteLine($"{a.Nome}: média {a.Media}");
class Aluno
{
public string Nome { get; set; } = "";
public double Nota1 { get; set; }
public double Nota2 { get; set; }
public double Media => (Nota1 + Nota2) / 2;
}
$ dotnet run p3.cs Bia: média 8,5 Bia: média 7 « a Média foi recalculada sozinha depois da troca da nota
- public double Nota1 { get; set; }
- Uma auto-propriedade:
get;eset;sem corpo. O compilador cria um campo de apoio escondido, sem nome que você possa usar. Por fora, é uma propriedade completa. - { get; set; } = "";
- O valor inicial vem depois das chaves, com
;no fim. Sem ele, umstringnascerianulle geraria o avisoCS8618, igual aos campos. - public double Media => (Nota1 + Nota2) / 2;
- Uma propriedade calculada: não guarda nada; cada leitura de
a.Mediafaz a conta na hora, com as notas atuais. É o mesmo quepublic double Media { get { return (Nota1 + Nota2) / 2; } }. Ela só temget, então ninguém consegue gravar nela.
public, se a auto-propriedade não tem regra?
Porque amanhã ela pode ganhar uma. Se Nota1 é uma propriedade, você pode trocar o set; por um set que recusa notas acima de 10, e nenhuma linha que usa a.Nota1 precisa mudar. Com um campo, mudar para propriedade depois quebra outras partes de programas maiores. Por isso a regra do C#: o que é public é propriedade; campos ficam private.
Propriedades só de leitura
Às vezes, quem está fora pode ler mas não pode mudar. Há três jeitos:
| Escrita | Quem pode gravar | Exemplo de uso |
|---|---|---|
public int Cliques { get; private set; } | Só os métodos da própria classe | Um contador que só aumenta pelo Somar() |
public decimal Total => Preco * Quantidade; | Ninguém: é calculada | Média, total, idade a partir do ano de nascimento |
public string Codigo { get; } | Só o construtor, ao criar o objeto | Um código que nunca muda depois de criado |
O private set é o contador da aula de campos, agora sem o método Valor():
Contador c = new Contador();
c.Somar();
c.Somar();
Console.WriteLine($"Cliques: {c.Cliques}");
class Contador
{
public int Cliques { get; private set; }
public void Somar()
{
Cliques++;
}
}
$ dotnet run p5.cs Cliques: 2
De fora, c.Cliques se lê normalmente. Uma linha c.Cliques = 100; no topo seria recusada com o erro CS0272 (veja Quando dá errado). Existe ainda o init, que deixa gravar só no momento da criação do objeto: ele está em readonly e init.
A palavra-chave field C# 14
Até o C# 13, havia um salto grande entre as duas formas: ou a auto-propriedade, sem regra nenhuma, ou a propriedade completa, com o campo de apoio escrito à mão. Bastava querer uma conferência no set para ter de declarar o _preco.
O C# 14 trouxe a palavra field ("campo"): dentro de um get ou set, ela é o campo de apoio que o compilador cria sozinho. Você escreve só o acessor que tem regra, e deixa o outro como get;:
Produto p = new Produto();
p.Preco = 7.90m;
Console.WriteLine($"Preço: {p.Preco:C}");
p.Preco = -5m;
class Produto
{
public decimal Preco
{
get;
set
{
if (value < 0)
throw new ArgumentException("O preço não pode ser negativo.");
field = value;
}
}
}
$ dotnet run p6.cs; echo "código: $?" Preço: R$ 7,90 Unhandled exception. System.ArgumentException: O preço não pode ser negativo. at Produto.set_Preco(Decimal value) in /home/ana/cs/p6.cs:line 14 at Program.<Main>$(String[] args) in /home/ana/cs/p6.cs:line 4 código: 134
- get;
- Sem corpo, como numa auto-propriedade: devolve o campo de apoio.
- field = value;
- Grava no campo de apoio automático. Nenhum
private decimal _preco;foi declarado. - throw new ArgumentException(…)
- Lança uma exceção com a mensagem escolhida: o programa para ali mesmo, como em todo erro não tratado (código 134).
ArgumentException= "argumento inválido". Você vai aprender a lançar e a tratar exceções em throw e try e catch. - at Produto.set_Preco(Decimal value) … line 14
- Por dentro, o
setdePrecoé um método chamadoset_Preco(e oget,get_Preco). A pilha mostra: quebrou na linha 14, dentro doset, chamado pela linha 4 (p.Preco = -5m;).
Também dá para usar field com =>: set => field = value < 0 ? 0 : value; troca preços negativos por zero (o ? : é o operador ternário).
Em versões antigas, field não existe. Pedindo o C# 13 ao compilador, com a linha #:property LangVersion=13 no topo do arquivo:
$ dotnet run p7.cs /home/ana/cs/p7.cs(8,20): error CS9260: O recurso "campo palavra-chave" não está disponível no C# 13.0. Use a versão da linguagem 14.0 ou superior. Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O "campo palavra-chave" é a tradução (ao pé da letra) de field keyword. Em código feito antes do .NET 10, você vai encontrar sempre o campo de apoio escrito à mão, como no p2.cs.
field?
Dentro de um acessor, field passou a ser a palavra-chave. Se a classe tinha um campo de verdade chamado field, o compilador avisa:
$ dotnet run q3.cs /home/ana/cs/q3.cs(8,27): warning CS9258: Na versão da linguagem 14.0, a palavra-chave ''field'' é associada a um campo de suporte sintetizado para a propriedade. Para evitar a geração de um campo de suporte sintetizado e para se referir ao membro existente, use 'this.field' ou '@field'.
Ou seja: ali, field já não é o seu campo, e sim o campo de apoio automático ("sintetizado"). Para falar do campo antigo, escreva this.field ou @field. É raro, mas pode aparecer ao atualizar um projeto antigo para o C# 14.
Formas diferentes e qual usar
| Você quer… | Escrita |
|---|---|
| um dado simples, sem regra | public string Nome { get; set; } = ""; |
| uma regra ao gravar C# 14 | public decimal Preco { get; set { … field = value; } } |
| uma regra ao gravar (antes do C# 14, ou quando o campo é usado por outros métodos) | private decimal _preco; + get => _preco; set { … _preco = value; } |
| só a classe pode mudar | public int Cliques { get; private set; } |
| um valor que é sempre uma conta | public double Media => (Nota1 + Nota2) / 2; |
| gravar só ao criar | { get; } com construtor, ou { get; init; } (readonly e init) |
| esconder de todo mundo | um campo private, sem propriedade |
Propriedade calculada ou método? Use propriedade quando o resultado é uma característica rápida de obter (Media, Total, Length). Use método quando é uma ação ou dá trabalho (Vender(), CalcularFrete(), SalvarNoArquivo()).
Quando dá errado
Aluno a = new Aluno();
a.Media = 10;
class Aluno
{
public double Nota1 { get; set; }
public double Nota2 { get; set; }
public double Media => (Nota1 + Nota2) / 2;
}
$ dotnet run p4.cs /home/ana/cs/p4.cs(2,1): error CS0200: A propriedade ou o indexador "Aluno.Media" não pode ser atribuído, pois é somente leitura Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que aconteceu: Media só tem get (é calculada). Não há set para receber o 10.
Como resolver: mude o que a média usa: as notas (a.Nota1 = 10; a.Nota2 = 10;). Se a propriedade realmente precisa ser gravada, ela deixa de ser calculada e ganha um set.
set é private
$ dotnet run p5.cs /home/ana/cs/p5.cs(5,1): error CS0272: A propriedade ou o indexador "Contador.Cliques" 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.
O que aconteceu: a linha 5 era c.Cliques = 100;, mas Cliques é { get; private set; }. Ler pode; gravar, só de dentro da classe. "Acessador" é o nome do get e do set.
Como resolver: use o método que a classe oferece (c.Somar()). O private set está ali de propósito.
Stack overflow: a propriedade chama a si mesma
Produto p = new Produto();
p.Preco = 7.90m;
Console.WriteLine(p.Preco);
class Produto
{
public decimal Preco
{
get { return Preco; }
set { Preco = value; }
}
}
$ dotnet run p8.cs; echo "código: $?" Stack overflow. Repeated 174521 times: -------------------------------- at Produto.set_Preco(System.Decimal) -------------------------------- at Program.<Main>$(System.String[]) código: 134
O que aconteceu: dentro do set, Preco = value; grava na própria propriedade, o que chama o set de novo, que grava na propriedade, que chama o set… É uma recursão sem fim. Cada chamada ocupa um pedaço da memória de chamadas (a "pilha"), até ela transbordar (stack overflow). O .NET mostra quantas vezes o set_Preco se repetiu (o número muda de uma execução para outra) e encerra o programa. O compilador não avisou nada.
No editor do guia, que roda o .NET em WebAssembly dentro do navegador, a saída é a mesma, com o Stack overflow. e o código 134. Só o número de repetições é outro (perto de 999), porque a pilha do navegador é menor.
Como resolver: grave num campo de apoio, que tem outro nome: _preco = value; (com private decimal _preco;), ou field = value; no C# 14. Ou, se não há regra, use a auto-propriedade { get; set; }.
string sem valor
$ dotnet run q2.cs /home/ana/cs/q2.cs(5,19): warning CS8618: O propriedade não anulável 'Nome' precisa conter um valor não nulo ao sair do construtor. Considere adicionar o modificador "obrigatório" ou declarar o propriedade como anulável. []
O que aconteceu: public string Nome { get; set; }, sem = "". É o mesmo aviso dos campos (com um "O propriedade" da tradução automática): o campo de apoio escondido nasce null.
Como resolver: { get; set; } = "";, string?, ou um construtor que exija o nome.
field numa versão antiga
$ dotnet run p7.cs /home/ana/cs/p7.cs(8,20): error CS9260: O recurso "campo palavra-chave" não está disponível no C# 13.0. Use a versão da linguagem 14.0 ou superior.
O que aconteceu: o projeto (ou o #:property LangVersion) pede uma versão anterior ao C# 14.
Como resolver: use o .NET 10 com a versão padrão, ou escreva o campo de apoio à mão.
Referência
| Escrita | Significado |
|---|---|
public T Nome { get; set; } | Auto-propriedade: ler e gravar, sem regra |
{ get; set; } = valor; | Auto-propriedade com valor inicial |
get { return _campo; } / get => _campo; | O que roda ao ler |
set { _campo = value; } / set => _campo = value; | O que roda ao gravar; value é o valor novo |
field C# 14 | Dentro de get/set: o campo de apoio automático |
{ get; private set; } | Todos leem; só a classe grava |
{ get; } | Só leitura; grava-se no construtor |
public T Nome => expressão; | Propriedade calculada (só get) |
get_Nome / set_Nome | Os nomes internos que aparecem nas exceções |
Missão final: o boletim calculado
Missão: o programa usa a.Media e a.Situacao, que ainda não existem (rode e veja o CS1061). Acrescente à classe duas propriedades calculadas com =>: Media, a média das duas notas, e Situacao, que vale "Aprovada" se a média for 6 ou mais e "Reprovada" se não (dica: o ternário Media >= 6 ? "Aprovada" : "Reprovada").
Aluno bia = new Aluno();
bia.Nome = "Bia";
bia.Nota1 = 8.0;
bia.Nota2 = 9.0;
Aluno caio = new Aluno();
caio.Nome = "Caio";
caio.Nota1 = 4.0;
caio.Nota2 = 6.0;
foreach (Aluno a in new Aluno[] { bia, caio })
Console.WriteLine($"{a.Nome}: {a.Media} ({a.Situacao})");
class Aluno
{
public string Nome { get; set; } = "";
public double Nota1 { get; set; }
public double Nota2 { get; set; }
}
Aluno bia = new Aluno();
bia.Nome = "Bia";
bia.Nota1 = 8.0;
bia.Nota2 = 9.0;
Aluno caio = new Aluno();
caio.Nome = "Caio";
caio.Nota1 = 4.0;
caio.Nota2 = 6.0;
foreach (Aluno a in new Aluno[] { bia, caio })
Console.WriteLine($"{a.Nome}: {a.Media} ({a.Situacao})");
class Aluno
{
public string Nome { get; set; } = "";
public double Nota1 { get; set; }
public double Nota2 { get; set; }
public double Media => (Nota1 + Nota2) / 2;
public string Situacao => Media >= 6 ? "Aprovada" : "Reprovada";
}
Exercícios
1. Auto-propriedade
Complete a auto-propriedade Idade, que pode ser lida e gravada:
2. O valor novo
Dentro de um set, qual palavra guarda o valor que está sendo gravado?
3. Só a classe grava
Complete para que todos possam ler Pontos, mas só a própria classe possa mudar:
4. Em p.Preco = 9.90m;, sendo Preco uma propriedade, o que roda?
5. O que a palavra field do C# 14 representa dentro de um set?
6. Por que set { Preco = value; }, dentro da própria propriedade Preco, derruba o programa?
Resumo
- Propriedade = um "campo com porteiro": por fora,
p.Preco = 7.90m;; por dentro,get(ler) eset(gravar, comvalue). - O valor fica num campo de apoio
private: escrito à mão (_preco), automático (auto-propriedade) ou acessado comfieldC# 14. public string Nome { get; set; } = "";é a forma mais comum. O que épublicé propriedade; campos ficamprivate.- Só leitura:
{ get; private set; },{ get; }e as calculadas com=>. Gravar nelas de fora:CS0200ouCS0272. - Nunca grave na própria propriedade dentro do
set:Stack overflow, código 134. - Próxima aula: Construtores, para cada objeto já nascer com os dados certos.