1. El problema de pasar datos
Supón que tenemos una app para un cole y necesitamos pasar info sobre un estudiante entre diferentes módulos: nombre, año de nacimiento, clase. ¿Qué solemos hacer?
Creamos una clase tipo:
public class Student
{
public string Name { get; set; }
public int YearOfBirth { get; set; }
public string Class { get; set; }
}
Parece lo de siempre. Pero este enfoque tiene varios problemas:
- Para comparar dos estudiantes por igualdad, por defecto solo se compara la referencia al objeto. O sea, dos estudiantes con los mismos campos pero objetos distintos ¡no son iguales!
- La clase se puede modificar después de crearla, lo que a veces lleva a errores (sobre todo si el objeto ya se está usando en algún sitio);
- Mucho código “boilerplate”: constructores, métodos de comparación, de copia (clones), ToString.
Y como ya te imaginas: ¡C# puede quitarnos estos dolores de cabeza! Te presento — record.
2. ¿Qué es un record?
record es un tipo especial en C# pensado justo para guardar datos. Un record tiene dos cosas clave:
- Inmutabilidad (immutability): los objetos tipo record por defecto son inmutables, o sea, sus propiedades se ponen una vez al crearlos y ya no cambian (bueno, los setters son privados). Aunque puedes declarar records mutables, por defecto — inmutables.
- Comparación por valor: si dos objetos record tienen los mismos valores en todos sus campos, se consideran iguales (== y .Equals() funcionan diferente!).
En realidad, record es el envoltorio perfecto para pasar datos entre capas de una app (por ejemplo, de la base de datos al controlador, del controlador a la vista, etc.).
3. Sintaxis de record
La forma más fácil — sintaxis posicional
Cuando solo queremos pasar un conjunto de valores, declaramos el tipo con una sola línea:
public record Student(string Name, int YearOfBirth, string Class);
¿Qué pasa por detrás? El compilador genera para nosotros:
- Propiedades automáticas solo de lectura (con setter privado);
- Constructor que recibe todos los parámetros;
- Métodos de comparación y copia;
- Un ToString guay que formatea bien la salida!
Usando un record posicional
Vamos a probar este nuevo tipo en nuestra app del cole:
var student1 = new Student("Iván", 2008, "8A");
var student2 = new Student("María", 2008, "8B");
El acceso a las propiedades es como siempre (solo que no puedes cambiarlas):
Console.WriteLine($"{student1.Name}, {student1.YearOfBirth}, {student1.Class}");
Intento de cambiar una propiedad después de crearla
student1.Name = "Pedro"; // ¡Error! Propiedad solo de lectura.
Si descomentas esa línea — el compilador se va a quejar enseguida: no se puede poner valor a una propiedad solo de lectura.
Así se ve el ToString generado automáticamente
Console.WriteLine(student1); // Muestra: Student { Name = Iván, YearOfBirth = 2008, Class = 8A }
¡Todo bonito y claro sin formatear a mano!
4. Comparando objetos record
Recuerda: si creas dos objetos distintos de una clase clásica con los mismos datos, igual no serán iguales entre sí:
var a = new Student("Iván", 2008, "8A");
var b = new Student("Iván", 2008, "8A");
Console.WriteLine(a == b); // Para clase: false
Pero si Student es un record, la igualdad funciona como esperas:
public record Student(string Name, int YearOfBirth, string Class);
var a = new Student("Iván", 2008, "8A");
var b = new Student("Iván", 2008, "8A");
Console.WriteLine(a == b); // Para record: ¡true!
O sea, dos records con los mismos campos se consideran iguales aunque sean dos objetos distintos en memoria.
5. ¿Cómo es por dentro?
A los estudiantes les sorprende cuánto hace el compilador por nosotros con los records. Vamos a comparar visualmente el código que tendríamos que escribir a mano para una clase y lo que hace un record.
La clásica clase hecha a mano
public class Student
{
public string Name { get; }
public int YearOfBirth { get; }
public string Class { get; }
public Student(string name, int yearOfBirth, string @class)
{
Name = name;
YearOfBirth = yearOfBirth;
Class = @class; //referencia a su propia clase
}
public override bool Equals(object? obj)
{
if (obj is not Student other) return false;
return Name == other.Name && YearOfBirth == other.YearOfBirth && Class == other.Class;
}
public override int GetHashCode()
{
return HashCode.Combine(Name, YearOfBirth, Class);
}
public override string ToString()
{
return $"Student {{ Name = {Name}, YearOfBirth = {YearOfBirth}, Class = {Class} }}";
}
}
No es raro que los programadores se vuelvan paranoicos — ¡es que escribimos lo mismo 10 veces!
Record — una sola línea
public record Student(string Name, int YearOfBirth, string Class);
6. Record e inmutabilidad: qué se puede y qué no
Los records por defecto tienen propiedades solo de lectura, y eso previene un montón de bugs. Pero si te empeñas (por ejemplo, si programas para una API muy antigua), puedes declarar records mutables:
public record MutableStudent
{
public string Name { get; set; }
public int YearOfBirth { get; set; }
public string Class { get; set; }
}
Ahora puedes cambiar estos campos, pero pierdes parte de las ventajas (como la seguridad, por ejemplo).
7. Desestructuración de record
Como la sintaxis posicional se parece mucho a las tuplas, puedes desestructurar un record fácilmente:
var student = new Student("Iván", 2008, "8A");
var (name, year, className) = student;
Console.WriteLine($"{name} - {year}, {className}"); // Iván - 2008, 8A
El compilador genera el método Deconstruct para cada record posicional, y eso hace la vida más fácil con LINQ, patrones switch y en general.
8. ¿Record es una clase o un struct?
Por defecto, record es un tipo por referencia, como una clase. O sea, todo lo típico de los tipos por referencia (almacenamiento en el heap, copiar la referencia, etc.) funciona igual.
Si quieres un tipo por valor (value type), en C# también puedes hacerlo — solo tienes que escribir:
public record struct Point(int X, int Y);
Pero para pasar datos casi siempre se usa la forma clásica de records, o sea, tipo por referencia. Más sobre record struct — en las próximas lecciones :P
Comparación entre class, struct, record
| Tipo | Inmutabilidad por defecto | Comparación por valor | Desestructuración fácil | Auto ToString |
|---|---|---|---|---|
| class | No | No (por referencia) | No | No |
| struct | No | Sí | No | No |
| record | Sí | Sí | Sí | Sí |
9. Particularidades y errores típicos
Muchos novatos se lían con los matices de los records. Por ejemplo, a veces esperan que cambiar un campo en un objeto record cambie otro (como pasa con clases y referencias). ¡No! Además, cuando usas with, recuerda que siempre se crea una copia, no se modifica el original. Los records son ideales para lógica donde tu app cuida la limpieza y previsibilidad de los datos.
Sí, si declaras campos con init en vez de set, también solo puedes ponerlos al crear el objeto o usando with, pero no después.
GO TO FULL VERSION