CodeGym /Cursos /C# SELF /Introducción a record...

Introducción a record y sintaxis posicional

C# SELF
Nivel 19 , Lección 1
Disponible

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; }
}
Clase para guardar datos de un estudiante (el método antiguo)

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:

  1. 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.
  2. 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);
Sintaxis posicional de record

¿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!
Comparación de records por valor

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);
Record — ¡lo mismo pero en una línea!

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
Desestructuración de un record posicional

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);
Record struct — tipo de valor record

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 No No
record

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.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION