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.

Você vai aprender
  • Por que campos public são perigosos e como uma propriedade os protege.
  • A anatomia de uma propriedade: get, set, a palavra value e 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 field do C# 14: validar sem escrever o campo de apoio.
  • Os erros reais: CS0200, CS0272, CS8618, CS9260 e o Stack overflow de 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 com get e set. 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 =. Em p.Preco = -5m;, value vale -5. Você não declara value: ele já existe em todo set.
_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.

Recusar em silêncio, avisar ou lançar uma exceção?

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; e set; 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, um string nasceria null e geraria o aviso CS8618, igual aos campos.
public double Media => (Nota1 + Nota2) / 2;
Uma propriedade calculada: não guarda nada; cada leitura de a.Media faz a conta na hora, com as notas atuais. É o mesmo que public double Media { get { return (Nota1 + Nota2) / 2; } }. Ela só tem get, então ninguém consegue gravar nela.
Por que não um campo 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:

EscritaQuem pode gravarExemplo de uso
public int Cliques { get; private set; }Só os métodos da própria classeUm contador que só aumenta pelo Somar()
public decimal Total => Preco * Quantidade;Ninguém: é calculadaMédia, total, idade a partir do ano de nascimento
public string Codigo { get; }Só o construtor, ao criar o objetoUm 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 set de Preco é um método chamado set_Preco (e o get, get_Preco). A pilha mostra: quebrou na linha 14, dentro do set, 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.

E se a classe já tiver algo chamado 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 regrapublic string Nome { get; set; } = "";
uma regra ao gravar C# 14public 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 mudarpublic int Cliques { get; private set; }
um valor que é sempre uma contapublic double Media => (Nota1 + Nota2) / 2;
gravar só ao criar{ get; } com construtor, ou { get; init; } (readonly e init)
esconder de todo mundoum 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

CS0200: gravar numa propriedade só de leitura
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.

CS0272: o 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; }.

CS8618: auto-propriedade 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.

CS9260: 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

EscritaSignificado
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# 14Dentro 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_NomeOs 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:

public int Idade { }

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:

public int Pontos { get; }

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) e set (gravar, com value).
  • O valor fica num campo de apoio private: escrito à mão (_preco), automático (auto-propriedade) ou acessado com field C# 14.
  • public string Nome { get; set; } = ""; é a forma mais comum. O que é public é propriedade; campos ficam private.
  • Só leitura: { get; private set; }, { get; } e as calculadas com =>. Gravar nelas de fora: CS0200 ou CS0272.
  • 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.