Una de las grandes novedades que se presentaron con .NET 5 y C# 9, fueron los registros.
Antes de eso podíamos trabajar con dos tipos de estructuras para almacenar información: class
y struct
.
Nota: existen más tipos de estructuras en C#: delegados, interfaces, etc... pero su objetivo no es almacenar información, que es lo que nos importa a efectos de este artículo y es en lo que nos vamos a centrar.
Con C# 9 o posterior disponemos de un tercer elemento para almacenar información: record
, también conocido como registro.
¿Y cuál es la diferencia con los dos anteriores que seguramente ya conoces?
Con las clases y las estructuras tenemos el problema de que pueden ser alterados. Los objetos de tipo clase son tipos por referencia, mientras que las estructuras son tipos por valor, que lo más que se podían acercar a un objeto inmutable era declarándolas como readonly
.
Los objetos de tipo record
, son objetos por referencia que vienen a solucionar el problema existente a la hora de generar objetos inmutables, esto es, objetos que no pueden variar. Por otro lado están "a caballo" entre clases y estructuras, puesto que tienen características de los dos.
Las similitudes con ambos elementos, los vemos inmediatamente al realizar una comparación entre dos registros.
- Podremos emplear el operador de igualdad
==
, puesto que al ser tipos por referencia nos va a indicar si se tratan de objetos con la misma referencia o no.
- Al igual que con las estructuras, el método
Equals
nos va a decir si son iguales o no, en función de los valores que tiene.
Pero veamos de forma más clara su funcionamiento mediante un ejemplo simple.
En primer lugar vamos a crear un nuevo tipo Persona
, pero en lugar de emplear un clase (con class
), emplearemos record
:
public record Persona {
public string Nombre { get; set; }
public string Apellidos { get; set; }
public Persona (string nombre, string apellidos) {
Nombre = nombre;
Apellidos = apellidos;
}
}
Una vez tenemos nuestro registro, vamos crear varios objetos de este tipo:
- Dos de ellos serán copias uno del otro.
- Un tercer objeto nuevo, pero con los mismos valores.
- Un último objeto con diferentes valores.
A continuación vamos a realizar comparaciones entre ellos para verificar:
- Objetos copiados directamente, al igual que las clases, mantienen la misma referencia.
- Objetos con los mismos valores, se reportan como iguales.
var persona1 = new Persona ("Rubén", "Rubio");
var persona2 = persona1;
var persona3 = new Persona ("Rubén", "Rubio");
var persona4 = new Persona ("Rubén", "R.");
Console.WriteLine ($"Referencia: persona1 = persona2 {ReferenceEquals(persona1,persona2)}");
Console.WriteLine ($"Valor: persona1 = persona2 {persona1.Equals(persona2)}");
Console.WriteLine ($"Referencia: persona1 = persona3 {ReferenceEquals(persona1,persona3)}");
Console.WriteLine ($"Valor: persona1 = persona3 {persona1.Equals(persona3)}");
Console.WriteLine ($"Referencia: persona1 = persona4 {ReferenceEquals(persona1,persona4)}");
Console.WriteLine ($"Valor: persona1 = persona4 {persona1.Equals(persona4)}");
Y el resultado de ejecutar el código.
Declaración mediante registros posicionales
Al principio decíamos que vienen a solucionar la definición de tipos inmutables, pero... con lo que hemos visto hasta ahora es posible modificar su contenido, por lo que no son inmutables 🤔
La declaración mediante registros posicionales nos va a permitir, por un lado simplificar el cuerpo del registro y, por otro, crear un registro realmente inmutable, siendo el propio compilador el que genere por nosotros toda la fontanería de constructor, deconstructores y propiedades.
Veamos cómo reescribiríamos el registro Persona
para hacerlo inmutable:
public record Persona (string Nombre, string Apellidos);
El ejemplo anterior sería equivalente a este otro código:
public record Persona {
public string Nombre { get; init; }
public string Apellidos { get; init; }
public Persona (string nombre, string apellidos) {
Nombre = nombre;
Apellidos = apellidos;
}
public void Deconstruct (out string nombre, out string apellidos) {
nombre = Nombre;
apellidos = Apellidos;
}
}
Si lo observamos detenidamente, las propiedades no tienen un set
, sino que son accesibles únicamente en la inicialización, puesto que son propiedades inicializadoras. De esta forma, ya no podremos alterar el registro siendo realmente inmutable.
Además, es considerable la reducción de código, puesto que hemos reducido toda la declaración a una única línea de código.
Pero, ¿y si necesitásemos constructores adicionales?
Para añadir más constructores a nuestro registro, bastará con añadirlos entre llaves a continuación de la declaración y siempre llamando al constructor base mediante el empleo de this
.
public record Persona (string Nombre, string Apellidos) {
public Persona (string Nombre) : this (Nombre, "") { }
};
Instanciación mediante expresiones con with
Por último veremos cómo podemos instanciar registros con ayuda de expresiones con with
, que no es más que una forma de generar un registro a partir de otro, previamente existente, al que indicaremos que alguna de sus propiedades debe tener un valor diferente.
En primer lugar, declararemos un registro a partir del cual copiar:
var persona1 = new Persona ("Rubén", "Rubio");
A continuación, realizaremos la copia mediante igualdad, pero añadiremos la partícula with
seguida de la declaración, entre llaves, de los valores que deben ser modificados:
var persona2 = persona1 with { Nombre = "Fernando" };
De esta forma tendremos una copia de persona1
en persona2
pero variando la propiedad Nombre
.
var persona1 = new Persona ("Rubén", "Rubio");
var persona2 = persona1 with { Nombre = "Fernando" };
Console.WriteLine ($"Persona 1: {persona1}");
Console.WriteLine ($"Persona 2: {persona2}");