C# -  Oito recursos dos Records


  Neste artigo vou apresentar oito recursos dos records na linguagem C#.

Os records são um novo tipo de referência de objeto imutável introduzido na versão 9.0 da linguagem C#. Eles são semelhantes às classes, mas possuem recursos adicionais para tornar mais fácil a criação de objetos simples e imutáveis.



Os records são a terceira maneira de definir tipos de dados em C#; os outros dois são : class e struct.

Como são um recurso novo, devemos passar algum tempo experimentando e tentando entender suas possibilidades e funcionalidades.

Neste artigo veremos oito propriedades dos records que você deve conhecer antes de utilizá-los, para tirar o melhor proveito deste novo tipo de dado.

Os exemplos de código foram feitos no C# 11 usando o recurso Top Level Statement.

1- Records são imutáveis

Por padrão, os records são imutáveis, e, uma vez criado, um record não pode ser modificado. Isso significa que todas as propriedades de um record são somente leitura, o que torna os records imutáveis e seguros para uso em threads.

O trecho de código a seguir ilustra isso:

var maria = new Pessoa("Maria", 1);

maria.Nome = "Maria Silveira"; //erro

public record Pessoa(string Nome, int Id);
 

O código acima não compila e emite uma mensagem de erro quando tentamos alterar a propridade Nome do record Pessoa.

2- Records implementam a igualdade

A outra propriedade dos records é que eles implementam a igualdade pronta para uso.

Em um record, a igualdade é determinada comparando-se o valor das propriedades, em vez de comparar as referências de objeto. Portanto, dois records são iguais se possuem os mesmos valores em suas propriedades.

var pessoa1 = new Pessoa("João", 30);
var
pessoa2 = new Pessoa("João", 30);
var
pessoa3 = new Pessoa("Maria", 25);

// Comparando duas instâncias de Pessoa com valores iguais
Console.WriteLine(pessoa1 == pessoa2);
// true

// Comparando duas instâncias de Pessoa com valores diferentes
Console.WriteLine(pessoa1 == pessoa3);
// false

Console.ReadKey();

public record Pessoa(string Nome, int Idade);
 

Nesse exemplo, criamos um record Pessoa com duas propriedades: Nome e Idade.

Em seguida, criamos duas instâncias desse record com valores iguais (pessoa1 e pessoa2) e uma terceira instância com valores diferentes (pessoa3).

Ao compararmos pessoa1 e pessoa2, obtemos true, pois os valores das propriedades desses records são iguais. Já ao compararmos pessoa1 e pessoa3, obtemos false, pois os valores das propriedades são diferentes.

Portanto, podemos concluir que a igualdade em records é determinada pelo valor das suas propriedades, o que torna mais fácil e intuitivo o processo de compará-los.

3- Records podem ser clonados ou atualizados usando a palavra-chave 'with'

Em um record, podemos clonar ou atualizar suas instâncias usando a palavra-chave with.

Para clonar um record, usamos a sintaxe with { }, que cria uma nova instância do record com as mesmas propriedades da instância original. Podemos usar isso para criar uma cópia de um record sem modificar o original.

var pessoa1 = new Pessoa("João", 30);

var pessoa2 = pessoa1 with { };

Console.WriteLine(pessoa1 == pessoa2); // true

Console.ReadKey();

public record Pessoa(string Nome, int Idade);
 

No exemplo, criamos um record Pessoa com duas propriedades (Nome e Idade) e uma instância desse record (pessoa1). Em seguida, criamos uma nova instância do record (pessoa2) clonando a instância original usando a sintaxe with { }.

Ao compararmos pessoa1 e pessoa2, obtemos true, pois ambas as instâncias têm os mesmos valores nas suas propriedades.

Além de clonar, podemos usar a palavra-chave with para atualizar as propriedades de um record. Para isso, usamos a mesma sintaxe with { }, mas incluímos as propriedades que queremos atualizar.

var pessoa1 = new Pessoa("João", 30);

var pessoa2 = pessoa1 with { Nome = "Maria" };

Console.WriteLine(pessoa1.Nome); // João

Console.WriteLine(pessoa2.Nome); // Maria

Console.ReadKey();

public record Pessoa(string Nome, int Idade);

 

Aqui, atualizamos a propriedade Nome da instância original de Pessoa (pessoa1) criando uma nova instância (pessoa2) com a propriedade atualizada. Ao executar o código, vemos que a propriedade Nome de pessoa1 permaneceu inalterada, enquanto a de pessoa2 foi atualizada para "Maria".

Assim a palavra-chave with é útil para clonar ou atualizar records, tornando o processo mais simples e legível.

4- Records podem ser classes ou structs

Os records são um tipo de referência, assim como as classes, mas também podem ser definidos como um tipo de valor, como as structs.

Exemplo de record definido como classe:


public
record Pessoa(string Nome, int Idade, string Email);
 

Neste exemplo, estamos definindo um record chamado Pessoa que contém três propriedades: Nome, Idade e Email. Essas propriedades são inicializadas por meio do construtor de record, que é gerado automaticamente. A seguir, podemos criar uma instância de Pessoa da seguinte maneira:


Pessoa pessoa = new Pessoa("João", 30, "joao@email.com");
 

Exemplo de record definido como struct:

public record Ponto(int X, int Y)
{
  
public Ponto Mover(int dx, int dy) => new Ponto(X + dx, Y + dy);
}

Neste exemplo, estamos definindo um record chamado Ponto que contém duas propriedades: X e Y. Além disso, estamos declarando um método Mover que cria e retorna uma nova instância de Ponto com as coordenadas atualizadas. A seguir, podemos criar uma instância de Ponto fazendo assim:


Ponto ponto =
new Ponto(10, 20);
 

5- Records podem ter subtipos

Os records podem ter subtipos, pois eles são um tipo de referência e podem herdar de outras classes ou interfaces. Isso significa que é possível criar uma hierarquia de classes de records que compartilham algumas propriedades e comportamentos comuns.

Por exemplo, considere a seguinte hierarquia de classes do tipo record que representam diferentes tipos de veículos:

public record Veiculo(string Marca, string Modelo);

public record Carro(string Marca, string Modelo, int Ano) : Veiculo(Marca, Modelo);

public record Moto(string Marca, string Modelo, int Cilindradas) : Veiculo(Marca, Modelo);
 

Neste exemplo, a classe base Veiculo define duas propriedades comuns a todos os veículos: Marca e Modelo. Em seguida, as classes Carro e Moto herdam de Veiculo e adicionam propriedades específicas de cada tipo de veículo: Ano para Carro e Cilindradas para Moto.

A seguir, podemos criar instâncias de Carro e Moto da seguinte maneira:

Carro carro = new Carro("Chevrolet", "Cruze", 2020);

Moto moto = new Moto("Honda", "CBR 600RR", 600);
 

Essa hierarquia de classes de records permite que o código seja mais genérico e reutilizável, já que é possível tratar todos os tipos de veículos como Veiculo, sem precisar se preocupar com as diferenças específicas entre eles.

Além disso, é importante notar que, assim como as classes e interfaces, os records podem implementar interfaces e, portanto, também podem fazer parte de hierarquias de tipos mais complexas.

6- Records podem ser abstract

Os records podem ser declarados como abstract ou não, assim como as classes comuns. No entanto, o uso de records abstratos é limitado, pois eles não podem ser instanciados diretamente.

Um record abstract pode ser útil para fornecer uma base comum para outras classes ou records que herdam suas propriedades, mas não podem ser instanciados por conta própria. Por exemplo:

public abstract record Pessoa(string Nome, int Idade);
 

Neste exemplo, estamos definindo um record abstrato chamado Pessoa que contém duas propriedades: Nome e Idade. Por ser abstrato, não podemos criar uma instância direta de Pessoa, mas podemos criar subclasses que herdam de Pessoa e adicionam mais propriedades ou comportamentos específicos, como:

public record Cliente(string Nome, int Idade, string Email) : Pessoa(Nome, Idade);

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

Aqui, estamos criando duas subclasses de Pessoa: Cliente e Funcionario, que herdam as propriedades Nome e Idade de Pessoa, mas adicionam propriedades adicionais específicas de cada tipo de pessoa.

É importante notar que o uso de records abstract é limitado, pois eles não suportam a implementação de métodos ou membros virtuais, o que pode tornar difícil a criação de hierarquias de classes mais complexas com records. Além disso, os records são tipicamente usados para representar dados imutáveis, então não faz muito sentido ter records abstratos com métodos que modificam suas propriedades.

Por isso, em geral, é mais comum usar records como tipos finais e definir classes abstratas para fornecer uma base comum para outras classes que herdam seus membros e comportamentos.

7- Records podem ser sealed

Os records podem ser declarados como sealed ou não. Um record sealed não pode ser herdado por outras classes ou records, enquanto um record não sealed pode ser herdado normalmente.

O uso de records sealed é útil em situações em que se deseja garantir que o record não será modificado ou estendido por outras classes. Por exemplo:

public sealed record Pessoa(string Nome, int Idade);
 

Neste exemplo, estamos definindo um record sealed chamado Pessoa que contém duas propriedades: Nome e Idade. Por ser sealed, não é possível criar uma subclasse de Pessoa e assim não poderemos criar subtipos.

Já em um exemplo de record não sealed, podemos ter:

public record Veiculo(string Marca, string Modelo);
 

Neste exemplo, estamos definindo um record chamado Veiculo que contém duas propriedades: Marca e Modelo. Como ele não é sealed, outras classes podem herdar de Veiculo e adicionar suas próprias propriedades e comportamentos.

É importante notar que o uso de records sealed deve ser feito com cuidado, pois ele impede a extensibilidade do tipo. Isso pode ser útil em situações em que se deseja garantir a imutabilidade dos dados ou a integridade do comportamento, mas pode ser prejudicial em outros casos em que a extensibilidade é desejada.

Em geral, é recomendável usar records sealed apenas em situações em que se tem certeza de que o record não precisa ser estendido por outras classes e quando se deseja garantir a imutabilidade dos dados. Caso contrário, é melhor usar records não sealed ou classes comuns que permitam a extensibilidade do tipo.

8 - Records podem ser decompostos

Outro recurso importante dos records é que eles podem ser decompostos e a decomposição de um record é uma maneira conveniente de extrair as propriedades de um record em variáveis separadas. Isso pode ser útil em situações em que se deseja trabalhar com cada propriedade individualmente ou passá-las como argumentos para métodos ou construtores.

Para fazer a decomposição de um record em C#, basta declarar as variáveis desejadas entre parênteses e atribuir o record a elas. Por exemplo:

// criar um record de pessoa
var
pessoa = new Pessoa("Maria", 30);

// fazer a decomposição do record em variáveis separadas
var (nome, idade) = pessoa;

// usar as variáveis
Console.WriteLine(
$"Nome: {nome}, Idade: {idade}");

Console.ReadKey();

public record Pessoa(string Nome, int Idade);
 

Neste exemplo, estamos criando um record de Pessoa com duas propriedades: Nome e Idade. Em seguida, fazemos a decomposição do record em duas variáveis separadas: nome e idade. O resultado é que as propriedades Nome e Idade do record são atribuídas às variáveis correspondentes, permitindo que as usemos separadamente.

Note que a ordem das variáveis na decomposição deve ser a mesma ordem em que as propriedades foram definidas no record.

A decomposição de records pode ser especialmente útil em situações em que se trabalha com coleções de records e se deseja acessar cada propriedade individualmente. Por exemplo:

var pessoas = new List<Pessoa> {
   
new Pessoa("Maria", 30),
   
new Pessoa("João", 25),
   
new Pessoa("Ana", 40)
};

foreach (var (nome, idade) in pessoas)
{
   Console.WriteLine(
$"Nome: {nome}, Idade: {idade}");
}

Console.ReadKey();

public record Pessoa(string Nome, int Idade);
 

Neste exemplo, estamos criando uma lista de pessoas e fazendo a decomposição de cada record em nome e idade dentro de um loop foreach. Isso permite imprimir as informações de cada pessoa separadamente, sem precisar acessar as propriedades do record diretamente.

Para concluir vamos apresentar os principais recursos dos records:

  1. Sintaxe simplificada: A sintaxe para definir records é mais simples do que a das classes. Por exemplo, você pode definir um record com apenas uma linha de código.
  2. Propriedades somente leitura: Uma vez criado, um record não pode ser modificado. Isso significa que todas as propriedades do record são somente leitura, o que torna os records imutáveis e seguros para uso em threads.
  3. Comparação por valor: Os records são comparados por valor, em vez de por referência. Isso significa que dois records são considerados iguais se todas as suas propriedades forem iguais, independentemente de onde eles foram criados ou como foram referenciados.
  4. Decomposição: É possível desestruturar um record em várias variáveis em uma única operação, o que torna a manipulação dos dados do record mais fácil e intuitiva.
  5. Herança: Os records podem ser derivados de outras classes e records, o que permite criar hierarquias de tipos mais complexas.
  6. Posicionais: Os records também podem ser definidos com propriedades posicionais, que permite acessar as propriedades com base em suas posições numéricas em vez de seus nomes.
  7. Expressões With : Os records também suportam expressões "with" que permitem criar uma nova instância do record com propriedades atualizadas, sem precisar alterar o original.

E estamos conversados ...

"Uns confiam em carros e outros em cavalos, mas nós faremos menção do nome do Senhor nosso Deus."
Salmos 20:7

Referências:


José Carlos Macoratti