C#  - Apresentando Records


 Hoje vamos apresentar o novo recurso do C# 9.0 chamado Records Types que permite tratar com tipos imutáveis.

Trabalhar com dados imutáveis é um recurso muito poderoso, pois geralmente leva a menos bugs e força você a transformar objetos em novos objetos em vez de modificar os objetos existentes.

Pois agora usando o C# 9.0 com .NET 5.0 podemos trabalhar com tipos imutáveis na linguagem C# usando os chamados Record Types ou Tipos de Registro, ou apenas Records ou Registros. Assim os records facilitam o trabalho com dados imutáveis no C#.

Além disso os tipos records se comportam como os tipos por valor no quesito comparação de igualdade.

Para iniciar vamos criar um projeto do tipo Console usando o .NET 5.0 RC1 no VS 2019 Community Preview:

Importante:  Para usar o C# 9.0 e conferir este recurso  você precisa instalar o .NET 5.0 RC1 e o Visual Studio 2019 Preview.  O arquivo de projeto .csproj do projeto console deve possuir a seguinte definição:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>  
    <OutputType>Exe</OutputType>
         <TargetFramework>net5.0</TargetFramework>
        <LangVersion>9.0</LangVersion>

  </PropertyGroup>

</Project>

No projeto criado vamos incluir uma classe Cliente com duas propriedades: Nome e Email

No código desta classe eu já estou usando o recurso init-only properties que apresentei neste artigo. Assim para entender como funciona este recurso leia o artigo.

Usando as propriedades init-only, você obtém propriedades imutáveis, e, na classe Cliente todas as suas propriedades são propriedades init-only, e, portanto, são imutáveis, você não pode alterá-las.

Isso significa que, ao trabalhar com esta classe, você não altera os valores das propriedades, porque você não pode. Se você precisar mudar algo, tem que criar um novo objeto Cliente com os dados atualizados. Isso é o que você faz ao trabalhar com dados imutáveis. Em vez de alterar um objeto ao longo do tempo, você cria um novo objeto quando uma alteração for necessária. Isso significa que seu objeto representa o estado dos dados em um ponto específico no tempo.

Vamos criar um objeto do tipo Cliente:

Agora, vamos supor que em algum momento o seu programa tenha que alterar o email dessa cliente para maria@yahoo.com

Ao trabalhar com dados imutáveis, você não pode alterar a propriedade Email, ao invés disso você tem que criar um novo objeto Cliente que representa o novo estado :

Observe que atribuímos o valor da propriedade Nome do primeiro objeto Cliente à propriedade Nome do segundo objeto Cliente  Nome = cliente.Nome;

Se a classe tiver mais propriedades essa abordagem vai ficar mais complexa de tratar.

Vamos supor que agora o Cliente possui 3 propriedades:

Vamos criar um novo objeto cliente atribuindo o mesmo nome e agora o seu sobrenome e depois alterando o seu email:

Notou que tivemos que incluir mais código para atribuir o sobrenome do cliente ?

Isso ocorre porque você também deve copiar essa propriedade do antigo objeto Cliente. Assim para uma classe com muitas propriedades vamos ter muito trabalho para realizar essa tarefa.  Infelizmente, o C#  nos força  a escrever esse código extra para criar tipos de referência imutáveis.

Aqui é que entra o novo recurso Records do C# 9.0.

Criando seu primeiro Record com C# 9.0

Vamos alterar nossa classe Cliente para um Record.

Para fazer isso basta incluir a palavra-chave record no lugar da palavra class:

Definir o tipo de Cliente como um tipo Record significa que você deseja tratar os objetos desse tipo como um valor de dados imutável.

Obs: Não somos obrigados a definir o init-only nas propriedades poderíamos também ter definido apenas o get conforme mostrado a seguir:

public record Cliente
{
    public string Nome { get; }
    public string Sobrenome { get; }
    public string Email { get; }
}

Aqui o tipo Record cria um tipo Cliente que contém 3 propriedades somente-leitura: Nome, Sobrenome e Email e o tipo Cliente é um tipo de referência.

E quando você define um tipo Record, o compilador sintetiza vários outros métodos para você:

- Métodos para comparações de igualdade baseadas em valores;
- Sobrescrita para GetHashCode();
- Membros Copy e Clone;
- PrintMembers e ToString();

Além disso definir um tipo como um Record fornece suporte para a nova expressão with que também foi introduzida com o C# 9.0 e que veremos a seguir.

Criando cópias com a expressão With

Vamos agora mostrar como realizar a mesma tarefa de alteração do email para o cliente desta vez usando Records e a expressão With.

Veja como fica :

A expressão with permite que você crie um novo objeto com mais eficiência.

No exemplo acima a última instrução no código usa a expressão with para criar um novo objeto Cliente a partir do objeto Cliente existente armazenado na variável cliente.

Você pode ler essa instrução assim:

"Use os valores de propriedade do objeto Cliente existente armazenado na variável cliente para criar um novo objeto Cliente e defina a propriedade Email desse novo objeto Cliente para "maria@yahoo.com",  a seguir armazene o novo objeto Cliente que é gerado pela expressão with com os valores da propriedade na variável novoCliente."

Note que a expressão with usa a sintaxe com chaves {}  que já conhecemos dos inicializadores de objeto para definir novos valores para propriedades específicas. Isso significa que, se você estiver familiarizado com os inicializadores de objetos, você vai assimilar essa nova sintaxe bem rápido. Mas lembre-se, a expressão with funciona apenas com Records Types e não com classes normais.

Verificando se dois Records são iguais

Os tipos Records são tipos de referência e não tipos de valor como as Structs.

Nos tipos Records temos o método Equals que pode ser usado comparar todos os valores das propriedades para uma igualdade. Além disso, o compilador também gera sobrecargas de operador para os operadores == e !=, de forma que esses operadores também usam esse método Equals.

Isso significa que você pode comparar dois Recods por seus valores de propriedade para uma igualdade como mostrado a seguir:

No código acima temos o seguinte:

- Criamos um objeto cliente do tipo Cliente;

- A seguir usando a expressão with criamos outro objeto do tipo Cliente chamado novoCliente a partir do objeto cliente existente, onde a propriedade Email foi alterada para 'maria@yahoo.com';

- Comparamos os objetos cliente e novoCliente usando o operador == obtendo o resultado false pois a propriedade Email possui valores diferentes;

- A seguir criamos outro objeto do tipo Cliente chamado outroCliente a partir do objeto novocliente existente usando a expressão with e alteramos o valor do email com o mesmo valor do objeto cliente original;

- Fizemos a comparação de cliente com outroCliente usando o operador == , e agora vamos obter true;

O resultado é visto a seguir:

Assim verificamos que a igualdade é um recurso poderoso dos tipos Records, e, para verificá-la basta chamar o método Equals ou usar o operador ==  que compara todos os valores das propriedades.

Na verdade, um tipo Record implementa a interface IEquality<T>, no caso do nosso tipo Cliente implementa IEquality<Cliente>.

Criando um Construtor e um Descontrutor

Talvez você queira trabalhar com seus records com um construtor em vez de usar inicializadores de objeto, e, também queira ter uma desconstrução posicional no seu tipo.

Para isso, você pode implementar um construtor e um método de desconstrução como vemos abaixo:

Agora usando o construtor e o desconstrutor definidos podemos ter um código conforme abaixo:

Como você pode ver, a desconstrução permite que você atribua o objeto Cliente a uma tupla que especifica as variáveis individuais para armazenar os valores. Se você não estiver interessado em um valor, pode usar um descarte, que é um sublinhado (_). Se você não precisar do sobrenome, por exemplo, você pode escrever a tupla assim: (primeiro, _ , último).

Agora, o importante é que toda essa construção e desconstrução também funcionaria se o tipo Cliente fosse uma classe em vez de um Record. O poder dos tipos Records - além da expressão with e membros gerados, como o método ToString substituído - é que o compilador C# também pode gerar todos os código do construtor e do desconstrutor para você, incluindo as propriedades init-only.

Gerando o Construtor, Desconstrutor e init-only properties

No código abaixo temos uma sintaxe abreviada para o nosso tipo Record  Cliente conhecida como Positional Record:


  public record Cliente(string Nome, string Sobrenome, string Email);
 

Neste código estamos criando :

  1. Um tipo Record de Cliente;
  2. Com as propriedades públicas init-only Nome, Sobrenome e Email;
  3. Com um construtor público parametrizado;
  4. Com um método de desconstrução;

Note que os parâmetro usam a notação PascalCase (começam com caractere maiúsculo) ao invés da notação camelCase porque as propriedades são geradas a partir desses parâmetros e na plataforma .NET propriedades são escritas com PascalCase.

Como em um construtor, você tem que passar os valores por posição para criar um objeto, assim essa é uma criação de objeto posicional. (Usando um object initializer a ordem não importa)

No Record definido acima temos um construtor que leva todos os valores da propriedade como parâmetros. Essa é a razão pela qual é chamado de Positional Record: você tem que passar todos os valores por posição para o construtor para criar um objeto.

Positional Records e a expressão with

Quando você usa a sintaxe Positional Record, conforme o código a seguir, você têm um construtor com três parâmetros e não existe mais o construtor padrão:


  public record Cliente(string Nome, string Sobrenome, string Email);
 

Você poderia ser levado a pensar que como não existe um construtor padrão a expressão with não pode ser usada. Mas ela pode ser usada.

Na verdade, o construtor de cópia protegida ainda existe e pode ser usado pela expressão with. Isso significa que um Positional Record como o criado acima, também pode ser usado com a expressão with conforme mostrado no exemplo abaixo:

Nota:  Este código esta usando o novo recurso do C# 9.0 chamado top-level program ou instrução de nível superior. Para saber mais leia sobre o recurso aqui.

Herdando Records de outro Records

A herança também funciona com tipos Record, e, você pode herdar seus records de outros records ou de object.

Assim:

O fragmento de código a seguir mostra dois tipos Record. O record Aluno herda do record Pessoa:

Tudo vai funcionar como você esperava e vamos tentar algumas coisas típicas que você geralmente faz quando trabalha com herança.

Por exemplo, vamos armazenar um objeto Aluno em uma variável pessoa do tipo Pessoa. Assim teremos acesso a todas as propriedades:

Já para criar um novo objeto do tipo Pessoa a expressão with permite que você defina apenas as propriedades Nome e Email que estão disponíveis na classe Pessoa. A propriedade Curso não está disponível no inicializador de objetos, pois é definida na classe Aluno, e aqui temos uma variável do tipo Pessoa.

Vamos então definir apenas o nome para o objeto novaPessoa:

Neste caso cabe a pergunta :  'A expressão with vai criar um novo objeto Pessoa ou um novo objeto Aluno ?

Felizmente a expressão with é inteligente o suficiente para descobrir em tempo de execução que a variável pessoa é do tipo Pessoa e  contém um objeto Aluno, portanto, ela cria um novo objeto Pessoa e não um novo objeto Aluno.

Veja o código abaixo exibindo o valor da propriedade Curso desse novo objeto Aluno no Console:

Isso funciona pois a expressão with chama o construtor de cópia para criar um novo objeto por meio do método <Clone>$ gerado pelo compilador.   

Temos assim uma apresentação de alguns dos principais recursos do tipo Record.

"Se alguém ouvir as minhas palavras e não as guardar, eu não o julgo; porque eu não vim para julgar o mundo, e sim para salvá-lo.
Quem me rejeita e não recebe as minhas palavras tem quem o julgue; a própria palavra que tenho proferido, essa o julgará no último dia."
João 12:47,48

Referências:


José Carlos Macoratti