C#

C# 9.0 - Propriedades init e record

Já estamos no C# 9.0, a nova versão lançada no .NET Conf 2020 trouxe uma série de novos recursos, veja neste artigo dois deles: propriedades init e record.

há 3 anos 5 meses

Formação Desenvolvedor C#
Conheça a formação em detalhes

Durante a .NET Conf deste ano (2020), a versão 5.0 do .NET foi lançada, junto também saiu a versão final C# 9.0. Como em a cada nova versão da linguagem, esta nona trouxe uma série de recursos que visam facilitar a vida do desenvolvedor e melhorar a legibilidade do código.

Neste e nos próximos artigos mostrarei um pouco desses recursos, começando pelas propriedades init e record.

Propriedades de inicialização

Introduzido na versão 3 do C#, inicializadores de objeto é um ótimo recurso que permite que o objeto seja criado de forma simples e clara. Permitindo inicializar todas as propriedades dele em uma única instrução. Um exemplo simples disso seria:

var pessoa = new Pessoa { Nome = "Carlos Silva", Idade = 33 };

Este recurso possibilita que o objeto seja criado sem a definição de um construtor. No caso do exemplo acima, a classe poderia ser declarada da seguinte forma:

public class Pessoa
{
    public string? Nome { get; set; }
    public int Idade { get; set; }
}

Mas uma grande limitação dele é que as propriedades dos objetos precisam ser mutáveis para que funcione. Isso ocorre porque inicialmente é chamado o construtor da classe (sem parâmetros no caso do exemplo) e só depois que os setters das propriedades são chamados.

Para resolver isso na versão 9.0 foi introduzido o conceito de “propriedades init”. Este tipo de propriedade utiliza o acessor init, que pode ser utilizado no lugar do set:

public class Pessoa
{
    public string? Nome { get; init; }
    public int Idade { get; init; }
}

Desta forma, a propriedade só pode ser inicializada utilizando o inicializador de objeto:

var pessoa = new Pessoa { Nome = "Alex Silva", Idade = 33 }; //Ok
pessoa.Idade = 32; //Será gerado um erro

Na prática significa que o estado do objeto não poderá ser alterado após a sua inicialização.

Acessor init com campos readonly

Como com o acessor init as propriedades só podem receber dados durante a incialização do objeto, ele permite a definição de campos readonly:

public class Pessoa
{
    private readonly string nome = "<desconhecido>";
    private readonly int idade = 0;
    
    public string Nome 
    { 
        get => nome; 
        init => nome = (value ?? throw new ArgumentNullException(nameof(Nome)));
    }
    public int Idade 
    { 
        get => idade; 
        init => idade = (value ?? throw new ArgumentNullException(nameof(Idade)));
    }
}

Desta forma teremos um comportamento parecido com o obtido quando os campos readonly são inicializados no construtor da classe.

Records (registros)

Um conceito base da programação orientada à objetos é que um objeto possui uma forte identidade e encapsula estados mutáveis ao longo da sua vida. Este tipo de conceito é facilmente aplicável no C#, entretanto as vezes você pode desejar o inverso, que um objeto seja imutável e quando isso ocorre o C# tende a atrapalhar esta implementação.

Para resolver este problema, na versão 9 foi introduzido o conceito de “record” (registro). Este novo tipo de objeto pode ser declarado como uma classe normal, bastando substituir a cláusula class por record:

public record Pessoa
{
    public string? Nome { get; init; }
    public int Idade { get; init; }
}

Um “record” ainda é uma classe, mas ao utilizar a cláusula record, o C# irá adicionar aos objetos dela alguns comportamentos de objetos value-type. A classe não deixa de ser um referece-type, mas na prática passa a ser bem parecida com estruturas. Ou seja, os objetos dela passam a serem definidos pelo seu conteúdo e não a sua identidade.

Mesmo que ainda seja possível criar “records” mutáveis, eles foram criados para melhorar o suporte a objetos imutáveis.

Cláusula with

Quando se trabalha com objetos imutáveis e é necessário representar um novo estado, pode ser um pouco trabalhoso criar novos objetos a partir de um objeto existente. Por exemplo, se for necessário alterar uma propriedade de um objeto imutável, deve-se criar um objeto que seja cópia de um existente, mas a única diferença entre eles será a propriedade que foi alterada. Esta técnica é chamada de mutação não destrutiva.

E para facilitar a implementação dela com “records”, também foi introduzido a cláusula with:

var pessoa = new Pessoa { Nome = "Alex Silva", Idade = 33 };
var novaPessoa = pessoa with { Idade = 32 };

Note que a expressão with acima está utilizando a sintaxe dos inicializados de objeto. Desta forma, ela só pode ser utilizada em propriedades que tenham definido o acessor set ou init.

Nos bastidores esta cláusula irá copiar todo o conteúdo do objeto original (pessoa) e alterar o valor das propriedades definidas na expressão with (Idade).

Igualdade

Um dos comportamentos de value-type adicionados aos “records” é o de igualdade. Todos os objetos do C# herdam o método Equals(object) da classe object e isso não é diferente para os “records”. Este método é utilizado quando os objetos estão sendo comparados.

Objetos reference-type, são comparados pela referência; e value-type, são comparados pelos valores. E mesmo que um “record” seja considerado um objeto reference-type, eles são comparados pelos valores:

var pessoa = new Pessoa { Nome = "Alex Silva", Idade = 33 };
var novaPessoa = pessoa with { Idade = 32 };
var outraPessoa = novaPessoa with { Idade = 33 };

Console.WriteLine(Object.ReferenceEquals(pessoa, outraPessoa));//False
Console.WriteLine(Object.Equals(pessoa, outraPessoa));//True

Com isso, se dois “records” possuírem os mesmos valores, eles serão considerados iguais. Este comportamento também irá ocorrer caso aplique os operadores == e !=, já que para manter a consistência, “records” implementam a interface IEquatable<T> e sobrescrevem esses operadores.

Mas é importante lembrar que igualdade e mudança de estado não combinam muito bem. Um problema dos “records” é que a alteração dos dados pode gerar à alteração do código retornado pelo método GetHashCode e se o objeto for salvo em uma tash table, também muda o resultado dela.

Então é necessário ter cuidado quando se altera um “record”.

Herança

Assim como qualquer classe, “record” também podem ser herdados por outros “records”:

public record Funcionario : Pessoa
{
    public int ID;
}

Os “records” filhos tem acesso à todas as propriedades do “record” pai:

var funcionario = new Funcionario { Nome = "José Silva", Idade = 44, ID = 1234 };

Que também podem ser acessadas com a expressão with:

var outroFuncionario = funcionario with { Nome = "Maria Santos", ID = 2345 };

E podem ser comparadas com o operador de igualdade:

Console.WriteLine(funcionario == outroFuncionario);//False

Record posicional

Nos exemplos anteriores todos os nossos objetos “record” foram criados utilizando o inicializador de objetos ou a expressão with, mas também é possível definir um construtor, e até um desconstrutor, no “record”:

public record Pessoa
{
    public string? Nome { get; init; }
    public int Idade { get; init; }
    public Pessoa (string nome, int idade) 
      => (Nome, Idade) = (nome, idade);
    public Descontruct (out string nome, out int idade) 
      => (Nome, Idade) = (nome, idade);
}

Caso o construtor e o desconstrutor definidos receber por parâmetro valores para todas as propriedades do “record”, a sua declaração pode ser encurtada para:

public record Pessoa (string Nome, int Idade);

Nos bastidores o C# se encarregará de criar as propriedades, o construtor e desconstrutor do “record” definido:

var pessoa = new Pessoa("Carlos Silva", 33); //Construtor
var (nome, idade) = pessoa;//Desconstrutor

É importante frisar que as propriedades serão definidas com o acessor init. E caso queira ter mais controle sobre elas, você pode desabilitar a auto geração definindo propriedades com o mesmo nome no bloco do “record”:

public record Pessoa (string Nome, int Idade)
{
    public string? Nome { get; init; } = Nome;
    public int Idade { get; init; } = Idade;
}

Neste caso serão gerados apenas o construtor e desconstrutor.

Este tipo de declaração reduzida também pode ser utilizada durante a herança:

public record Funcionario (string Nome, int Idade, int ID) : Pessoa(Nome, Idade);

Com isso finalizamos este artigo. No próximo abordarei outros recursos do C# 9.0. Até lá!

Autor(a) do artigo

Wladimilson M. Nascimento
Wladimilson M. Nascimento

Instrutor, nerd, cinéfilo e desenvolvedor nas horas vagas. Graduado em Ciências da Computação pela Universidade Metodista de São Paulo.

Todos os artigos

Artigos relacionados Ver todos