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.

Você vai aprender
  • O que é um campo readonly e onde ele pode receber valor (CS0191).
  • A diferença entre const, readonly e static readonly (CS0133).
  • A pegadinha: uma lista readonly ainda aceita itens novos.
  • Propriedades só com get, preenchidas no construtor (CS0200).
  • O acessador init C# 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;
readonly vem 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, string e enums escritos ali mesmo. É da classe (como static): Loja.Nome.
public static readonly string Pasta = Path.Combine(…);
Calculado quando o programa roda, uma vez só, e é da classe. Path.Combine monta 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 = …;.

conststatic readonlyreadonly
Quando o valor é decididoNa compilaçãoQuando o programa roda (uma vez)Quando cada objeto é criado
Um para…A classeA classeCada objeto
Onde recebe valorSó na declaraçãoNa declaração ou no construtor staticNa declaração ou no construtor
Tipos aceitosNúmeros, char, bool, string, enumQualquer umQualquer um
Exemploconst 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, um init: a propriedade pode receber valor só durante a criação do objeto (nas chaves do new ou 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.

AcessadorNo construtorNas chaves do newDepois de criado
{ get; set; }SimSimSim
{ get; init; }SimSimNão (CS8852)
{ get; }SimNãoNão (CS0200)
{ get; private set; }SimSó de dentro da classeSó 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çãoUse
Valor fixo, conhecido ao escrever o código (7 dias, o nome da loja)const
Valor da classe, calculado quando o programa rodastatic readonly
Campo privado que cada objeto recebe no construtor e nunca mudaprivate 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

error CS0191: Não é possível atribuir um campo somente leitura…

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.

error CS0133: A expressão que está sendo atribuída a "…" deve ser constante

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.

error CS8852: A propriedade somente de inicialização … só pode ser atribuído em um inicializador de objeto…

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.

A lista readonly continua mudando

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

EscritaSignificado
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# 9Propriedade 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.
  • readonly protege a variável, não o objeto: uma lista readonly ainda aceita Add.
  • { get; }: só o construtor dá valor (CS0200 depois). { get; init; } C# 9: o construtor ou as chaves do new (CS8852 depois).
  • O aviso CS8618 diz que uma propriedade pode ficar null. A solução moderna é o required, da próxima aula.