C# readonly e init
Alguns dados nunca deveriam mudar depois que o objeto nasce: o CPF de uma pessoa, o número de uma nota fiscal, o código de barras de um produto. Em Encapsulamento você viu que a classe pode simplesmente não oferecer um set. Nesta aula você ganha duas palavras que pedem ao compilador para garantir isso: readonly ("somente leitura"), para campos, e init ("inicialização"), para propriedades.
- O que é um campo
readonlye onde ele pode receber valor (CS0191). - A diferença entre
const,readonlyestatic readonly(CS0133). - A pegadinha: uma lista
readonlyainda aceita itens novos. - Propriedades só com
get, preenchidas no construtor (CS0200). - O acessador
initC# 9: valor dado na criação e nunca mais (CS8852). - O aviso
CS8618, que prepara a próxima aula.
A ideia: a certidão de nascimento
A certidão de nascimento da Ana foi escrita uma vez, no cartório, no dia em que ela foi registrada. Depois disso, ninguém pega uma caneta e muda a data ou o nome dos pais. Se alguma coisa precisar mudar de verdade, o cartório emite uma certidão nova.
Um dado readonly é assim: ele é escrito no "cartório" (o construtor) e, daí em diante, só pode ser lido. A diferença para "não ter set" é que, agora, nem a própria classe consegue mudar o valor por engano. O compilador vigia.
Campos readonly
var cpf = new Documento("123.456.789-09");
Console.WriteLine(cpf.Numero());
class Documento
{
private readonly string _numero;
public Documento(string numero)
{
_numero = numero;
}
public string Numero()
{
return _numero;
}
}
$ dotnet run documento.cs 123.456.789-09
- private readonly string _numero;
readonlyvem depois do modificador de acesso. O campo só pode receber valor em dois lugares: aqui mesmo, na declaração (= "…"), ou dentro de um construtor da classe.- _numero = numero; no construtor
- O "cartório". É permitido, e pode até ser feito mais de uma vez dentro do construtor.
- public string Numero()
- Ler um campo
readonlyé sempre permitido.
Tentando mudar depois
Agora alguém da própria classe tenta trocar o número num método:
var cpf = new Documento("123.456.789-09");
Console.WriteLine(cpf.Numero());
class Documento
{
private readonly string _numero;
public Documento(string numero)
{
_numero = numero;
}
public string Numero()
{
return _numero;
}
public void Trocar(string novo)
{
_numero = novo;
}
}
$ dotnet run documento-erro.cs /home/ana/cs/documento-erro.cs(20,9): error CS0191: Não é possível atribuir um campo somente leitura (exceto em um construtor ou em um setter somente de inicialização do tipo no qual o campo esteja definido ou em um inicializador de variável) Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
A mensagem lista os lugares em que um campo readonly pode receber valor: o construtor, um init (você vai ver logo abaixo) e o inicializador da declaração. A linha 20 está num método comum, então é barrada. Repare que o erro vem até de dentro da própria classe: o private protege contra quem está de fora, e o readonly protege também contra quem está dentro.
Missão: este programa compila, mas o método Remarcar muda um preço que deveria ser fixo. Deixe o campo _preco readonly e rode: o compilador tem de apontar o erro CS0191 na linha do Remarcar. (Aqui, a missão é ver o erro.)
var etiqueta = new Etiqueta(19.90m);
etiqueta.Remarcar(5m);
Console.WriteLine(etiqueta.Preco());
class Etiqueta
{
private decimal _preco;
public Etiqueta(decimal preco)
{
_preco = preco;
}
public decimal Preco()
{
return _preco;
}
public void Remarcar(decimal novo)
{
_preco = novo;
}
}
var etiqueta = new Etiqueta(19.90m);
etiqueta.Remarcar(5m);
Console.WriteLine(etiqueta.Preco());
class Etiqueta
{
private readonly decimal _preco;
public Etiqueta(decimal preco)
{
_preco = preco;
}
public decimal Preco()
{
return _preco;
}
public void Remarcar(decimal novo)
{
_preco = novo;
}
}
Para consertar de verdade, apague o método Remarcar. Se o preço precisa mesmo mudar, crie uma etiqueta nova (new Etiqueta(5m)), como o cartório faz com a certidão.
const, readonly ou static readonly?
As três formas guardam valores que não mudam, mas em momentos diferentes:
var loja = new Loja(0.05m);
Console.WriteLine($"{Loja.Nome}: taxa de {loja.Taxa:P0}");
Console.WriteLine(Loja.Pasta);
class Loja
{
public const string Nome = "Lojinha da Ana";
public static readonly string Pasta = Path.Combine("dados", "vendas");
private readonly decimal _taxa;
public Loja(decimal taxa)
{
_taxa = taxa;
}
public decimal Taxa => _taxa;
}
$ dotnet run loja.cs Lojinha da Ana: taxa de 5% dados/vendas
- public const string Nome = "Lojinha da Ana";
- Uma constante: o valor é conhecido quando o programa é compilado e vai "colado" no código. Só aceita números,
char,bool,stringe enums escritos ali mesmo. É da classe (comostatic):Loja.Nome. - public static readonly string Pasta = Path.Combine(…);
- Calculado quando o programa roda, uma vez só, e é da classe.
Path.Combinemonta um caminho com a barra certa do sistema (no Linux,/). - private readonly decimal _taxa;
- Sem
static: cada loja tem a sua taxa, dada no construtor, e ela nunca muda depois.
Se você tenta usar const com algo que só se sabe quando o programa roda:
Console.WriteLine(Loja.Pasta);
class Loja
{
public const string Pasta = Path.Combine("dados", "vendas");
}
$ dotnet run const-erro.cs /home/ana/cs/const-erro.cs(5,33): error CS0133: A expressão que está sendo atribuída a "Loja.Pasta" deve ser constante Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O compilador não roda Path.Combine para descobrir o resultado; ele precisa de um valor pronto. Conserto: public static readonly string Pasta = …;.
const | static readonly | readonly | |
|---|---|---|---|
| Quando o valor é decidido | Na compilação | Quando o programa roda (uma vez) | Quando cada objeto é criado |
| Um para… | A classe | A classe | Cada objeto |
| Onde recebe valor | Só na declaração | Na declaração ou no construtor static | Na declaração ou no construtor |
| Tipos aceitos | Números, char, bool, string, enum | Qualquer um | Qualquer um |
| Exemplo | const int DiasDaSemana = 7; | static readonly string Pasta = …; | readonly string _cpf; |
A pegadinha: readonly protege a variável, não o objeto
A Ana deixou a lista de compras readonly e ficou surpresa: os itens continuam entrando.
var lista = new ListaDeCompras();
lista.Adicionar("arroz");
lista.Adicionar("feijão");
lista.Mostrar();
class ListaDeCompras
{
private readonly List<string> _itens = new List<string>();
public void Adicionar(string item)
{
_itens.Add(item);
}
public void Mostrar()
{
Console.WriteLine(string.Join(", ", _itens));
}
}
$ dotnet run lista.cs arroz, feijão
Não é um bug. Uma lista é um tipo de referência (veja Valor × Referência): o campo _itens guarda o "endereço" de uma lista, e readonly só impede trocar esse endereço. A lista em si continua aceitando Add. Tente trocar a lista inteira:
$ dotnet run lista-erro.cs « com um método que faz _itens = new List<string>(); /home/ana/cs/lista-erro.cs(22,9): error CS0191: Não é possível atribuir um campo somente leitura (exceto em um construtor ou em um setter somente de inicialização do tipo no qual o campo esteja definido ou em um inicializador de variável) Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Pense numa casa com endereço fixo: você não pode mudar a casa de rua, mas pode trocar os móveis lá dentro. Para que nem os móveis mudem, existem as coleções somente leitura, como IReadOnlyList<T>, vistas em Coleções.
Propriedades só com get
O mesmo efeito do readonly vale para propriedades: uma propriedade automática com só get pode receber valor no construtor, e nunca mais. É o jeito mais comum de escrever dados que não mudam:
var p = new Produto("Caderno", 12.50m);
Console.WriteLine($"{p.Nome}: {p.Preco:C}");
p.Preco = 1m;
class Produto
{
public Produto(string nome, decimal preco)
{
Nome = nome;
Preco = preco;
}
public string Nome { get; }
public decimal Preco { get; }
}
$ dotnet run getonly.cs /home/ana/cs/getonly.cs(3,1): error CS0200: A propriedade ou o indexador "Produto.Preco" não pode ser atribuído, pois é somente leitura Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Por baixo dos panos, { get; } cria um campo escondido readonly. É o mesmo cartório: o construtor escreve, e o resto só lê.
init: valor na criação, e nunca mais C# 9
Escrever um construtor com um parâmetro para cada propriedade cansa, e na chamada new Produto("Caderno", 12.50m) nem fica claro qual número é o quê. O C# permite dar valores às propriedades no momento da criação, entre chaves, com os nomes à vista. Isso se chama inicializador de objeto e tem uma aula só para ele, Inicializadores e required. Aqui interessa o acessador que combina com ele: init.
var caderno = new Produto { Nome = "Caderno", Preco = 12.50m };
Console.WriteLine($"{caderno.Nome}: {caderno.Preco:C}");
class Produto
{
public string Nome { get; init; } = "";
public decimal Preco { get; init; }
}
$ dotnet run init.cs Caderno: R$ 12,50
- public decimal Preco { get; init; }
- No lugar do
set, uminit: a propriedade pode receber valor só durante a criação do objeto (nas chaves donewou no construtor). - = "";
- Valor inicial do
Nome, caso quem cria não diga nenhum. Sem ele, vem um aviso (veja abaixo). - new Produto { Nome = "Caderno", Preco = 12.50m }
- O inicializador de objeto: cria o produto e já preenche as propriedades, pelo nome.
Depois que o objeto está pronto, init se comporta como "sem set":
$ dotnet run init-erro.cs « com a linha caderno.Preco = 1m; depois da criação /home/ana/cs/init-erro.cs(3,1): error CS8852: A propriedade somente de inicialização ou o indexador 'Produto.Preco' só pode ser atribuído em um inicializador de objeto, em 'this' ou 'base' em um construtor de instância ou em um acessador 'init'. Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Em inglês: Init-only property or indexer 'Produto.Preco' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor.
| Acessador | No construtor | Nas chaves do new | Depois de criado |
|---|---|---|---|
{ get; set; } | Sim | Sim | Sim |
{ get; init; } | Sim | Sim | Não (CS8852) |
{ get; } | Sim | Não | Não (CS0200) |
{ get; private set; } | Sim | Só de dentro da classe | Só de dentro da classe |
O aviso CS8618
Se a Ana tirar o = "" do Nome, o programa roda, mas com um aviso:
$ dotnet run init-sem.cs /home/ana/cs/init-sem.cs(6,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. Caderno
O compilador lembra que nada obriga quem cria um Produto a dizer o nome: new Produto { Preco = 5m } deixaria Nome valendo null (veja Referências Anuláveis). O "modificador obrigatório" que a mensagem sugere é o required, assunto da próxima aula.
Qual usar?
| Situação | Use |
|---|---|
| Valor fixo, conhecido ao escrever o código (7 dias, o nome da loja) | const |
| Valor da classe, calculado quando o programa roda | static readonly |
| Campo privado que cada objeto recebe no construtor e nunca muda | private readonly |
| Dado público que nunca muda, preenchido pelo construtor | { get; } |
Dado público que nunca muda, preenchido nas chaves do new | { get; init; } |
Objetos que não mudam (os imutáveis) são mais fáceis de entender: o valor que você leu agora é o mesmo de amanhã. É por isso que a string do .NET é imutável, e que os records usam init por padrão.
Quando dá errado
O que aconteceu: um campo readonly recebeu valor fora do construtor, do init e da declaração.
Como resolver: se o valor deve mesmo mudar, tire o readonly e crie um método com validação. Se não deve, crie um objeto novo com o valor novo.
O que aconteceu: um const recebeu algo que só se calcula com o programa rodando (uma chamada de método, um new).
Como resolver: troque const por static readonly.
O que aconteceu: uma propriedade init recebeu valor depois de o objeto estar pronto.
Como resolver: passe o valor nas chaves do new. Para "mudar", crie outro objeto.
O que aconteceu: readonly impede trocar a lista, não mexer nela.
Como resolver: não exponha a lista para fora da classe; ofereça métodos (Adicionar) ou uma coleção somente leitura.
Referência
| Escrita | Significado |
|---|---|
private readonly string _cpf; | Campo que recebe valor só na declaração ou no construtor |
public static readonly string Pasta = …; | Valor da classe, calculado uma vez quando o programa roda |
public const int Dias = 7; | Constante de compilação |
public string Nome { get; } | Propriedade preenchida só no construtor |
public string Nome { get; init; } C# 9 | Propriedade preenchida no construtor ou nas chaves do new |
new Produto { Nome = "…", Preco = 1m } | Inicializador de objeto |
Missão final: o produto que não muda de preço
Missão: hoje o preço do caderno é trocado depois de criado, e a saída mostra R$ 1,00. Troque set por init nas duas propriedades e rode: o compilador vai apontar CS8852 na linha que muda o preço. Apague essa linha e rode de novo.
var caderno = new Produto { Nome = "Caderno", Preco = 12.50m };
caderno.Preco = 1m;
Console.WriteLine($"{caderno.Nome}: {caderno.Preco:C}");
class Produto
{
public string Nome { get; set; } = "";
public decimal Preco { get; set; }
}
var caderno = new Produto { Nome = "Caderno", Preco = 12.50m };
Console.WriteLine($"{caderno.Nome}: {caderno.Preco:C}");
class Produto
{
public string Nome { get; init; } = "";
public decimal Preco { get; init; }
}
Exercícios
1. Somente leitura
Qual palavra faz um campo só poder receber valor na declaração ou no construtor?
2. Só na criação
Complete a propriedade para que ela só receba valor na criação do objeto: public string Cor { get; ___ }
3. const com método
Qual é o código do erro ao escrever const string Pasta = Path.Combine("a", "b");?
4. Um campo private readonly List<string> _itens. O que é permitido num método comum da classe?
5. Com public decimal Preco { get; init; }, onde não dá para dar valor ao Preco?
6. Cada loja tem uma taxa diferente, dada ao criar a loja, que nunca muda. O que usar?
Resumo
readonly: o campo recebe valor só na declaração ou no construtor. Fora disso,CS0191, mesmo dentro da classe.consté decidido na compilação;static readonly, quando o programa roda (um para a classe);readonly, quando cada objeto nasce.readonlyprotege a variável, não o objeto: uma listareadonlyainda aceitaAdd.{ get; }: só o construtor dá valor (CS0200depois).{ get; init; }C# 9: o construtor ou as chaves donew(CS8852depois).- O aviso
CS8618diz que uma propriedade pode ficarnull. A solução moderna é orequired, da próxima aula.