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á!