CSharp

Tutorial CSharp: Records

Aprende a usar records en C# para datos inmutables con igualdad estructural y operador with. Ejemplos prácticos y ventajas en programación funcional.

Aprende CSharp y certifícate

Declaración y uso

Los records son un tipo de referencia introducido en C# 9 que están diseñados específicamente para trabajar con datos inmutables. A diferencia de las clases tradicionales, los records proporcionan una sintaxis concisa para crear tipos que se centran principalmente en almacenar datos.

La declaración de un record es similar a la de una clase, pero utiliza la palabra clave record en lugar de class. Veamos cómo se declara un record básico:

// Declaración de un record básico
public record Persona(string Nombre, int Edad);

Con esta simple línea, C# genera automáticamente:

  • Un constructor con parámetros para las propiedades declaradas
  • Propiedades inmutables públicas para cada parámetro
  • Métodos Equals() y GetHashCode() basados en los valores de las propiedades
  • Un método ToString() que muestra los nombres y valores de las propiedades

Para usar un record, simplemente creamos una instancia como lo haríamos con cualquier clase:

// Creación de una instancia de record
Persona persona = new Persona("Ana", 28);

// Acceso a las propiedades
Console.WriteLine($"Nombre: {persona.Nombre}, Edad: {persona.Edad}");

Los records también pueden incluir miembros adicionales como métodos, propiedades calculadas o constructores personalizados:

public record Empleado(string Nombre, int Edad, string Departamento)
{
    // Propiedad calculada
    public bool EsAdulto => Edad >= 18;
    
    // Método personalizado
    public string ObtenerDescripcion() => $"{Nombre} trabaja en {Departamento}";
}

Podemos usar estos miembros adicionales de la misma forma que lo haríamos con una clase:

Empleado empleado = new Empleado("Carlos", 35, "Desarrollo");
Console.WriteLine(empleado.ObtenerDescripcion());
Console.WriteLine($"¿Es adulto? {empleado.EsAdulto}");

También es posible declarar records con una sintaxis más detallada, similar a la de las clases:

public record Producto
{
    public string Nombre { get; init; }
    public decimal Precio { get; init; }
    
    public Producto(string nombre, decimal precio)
    {
        Nombre = nombre;
        Precio = precio;
    }
}

En este caso, usamos la palabra clave init para las propiedades, lo que permite establecer su valor durante la inicialización pero las hace inmutables después.

Los records son especialmente útiles en escenarios como:

  • Transferencia de datos (DTOs)
  • Modelos de dominio inmutables
  • Configuraciones que no deben cambiar
  • Mensajes en sistemas de comunicación

Un ejemplo práctico sería un sistema de gestión de pedidos:

// Definición de records para un sistema de pedidos
public record Producto(string Codigo, string Nombre, decimal Precio);
public record LineaPedido(Producto Producto, int Cantidad);
public record Pedido(string NumeroPedido, DateTime Fecha, List<LineaPedido> Lineas);

// Uso en código
Producto laptop = new Producto("LP001", "Laptop Pro", 1299.99m);
Producto mouse = new Producto("MS002", "Mouse Ergonómico", 49.99m);

LineaPedido linea1 = new LineaPedido(laptop, 1);
LineaPedido linea2 = new LineaPedido(mouse, 2);

List<LineaPedido> lineas = new List<LineaPedido> { linea1, linea2 };
Pedido pedido = new Pedido("P2023-001", DateTime.Now, lineas);

// Acceso a los datos
Console.WriteLine($"Pedido: {pedido.NumeroPedido}");
Console.WriteLine($"Fecha: {pedido.Fecha}");
Console.WriteLine("Productos:");

foreach (var linea in pedido.Lineas)
{
    Console.WriteLine($"- {linea.Producto.Nombre}: {linea.Cantidad} x {linea.Producto.Precio:C}");
}

Los records también pueden heredar de otros records, lo que permite crear jerarquías de tipos de datos inmutables:

public record Persona(string Nombre, int Edad);
public record Estudiante(string Nombre, int Edad, string Carrera) : Persona(Nombre, Edad);

// Uso
Estudiante estudiante = new Estudiante("Laura", 22, "Informática");
Console.WriteLine(estudiante); // Estudiante { Nombre = Laura, Edad = 22, Carrera = Informática }

La declaración de records proporciona una forma concisa y eficiente de trabajar con datos inmutables en C#, reduciendo significativamente la cantidad de código repetitivo que normalmente tendríamos que escribir para lograr la misma funcionalidad con clases tradicionales.

Inmutabilidad con with

Los records en C# están diseñados para trabajar con datos inmutables, lo que significa que una vez creados, sus propiedades no pueden modificarse. Sin embargo, a menudo necesitamos crear nuevas instancias basadas en otras existentes con pequeñas modificaciones. Para esto, C# introduce el operador with, una característica especial que facilita la creación de copias modificadas de records.

El operador with permite crear una nueva instancia de un record copiando todos los valores de propiedades de un record existente, pero cambiando solo los que especifiquemos. Esta técnica se conoce como copia no destructiva o inmutabilidad funcional.

Veamos cómo funciona con un ejemplo sencillo:

// Definimos un record para representar un punto
public record Punto(int X, int Y);

// Creamos una instancia
Punto p1 = new Punto(10, 20);

// Creamos una nueva instancia modificando solo X
Punto p2 = p1 with { X = 15 };

Console.WriteLine(p1); // Punto { X = 10, Y = 20 }
Console.WriteLine(p2); // Punto { X = 15, Y = 20 }

En este ejemplo, p2 es una nueva instancia que tiene el mismo valor de Y que p1, pero con un valor diferente para X. El record original p1 permanece sin cambios, respetando el principio de inmutabilidad.

La expresión with es especialmente útil cuando trabajamos con records que tienen muchas propiedades:

public record Producto(string Codigo, string Nombre, decimal Precio, string Categoria, bool Disponible);

Producto laptop = new Producto("LP001", "Laptop Pro", 1299.99m, "Electrónica", true);

// Creamos una versión con precio actualizado
Producto laptopRebajada = laptop with { Precio = 1099.99m };

// Creamos una versión que no está disponible
Producto laptopAgotada = laptop with { Disponible = false };

Podemos modificar múltiples propiedades en una sola expresión with:

// Modificamos varias propiedades a la vez
Producto laptopNuevoModelo = laptop with 
{ 
    Codigo = "LP002", 
    Nombre = "Laptop Pro 2023",
    Precio = 1499.99m 
};

El operador with también funciona con records anidados:

public record Direccion(string Calle, string Ciudad, string CodigoPostal);
public record Cliente(string Nombre, Direccion Direccion);

Direccion direccion = new Direccion("Calle Mayor", "Madrid", "28001");
Cliente cliente = new Cliente("Ana García", direccion);

// Creamos un cliente con la misma dirección pero diferente código postal
Cliente clienteNuevoCP = cliente with 
{ 
    Direccion = cliente.Direccion with { CodigoPostal = "28002" } 
};

Console.WriteLine(cliente);      // Cliente { Nombre = Ana García, Direccion = Direccion { Calle = Calle Mayor, Ciudad = Madrid, CodigoPostal = 28001 } }
Console.WriteLine(clienteNuevoCP); // Cliente { Nombre = Ana García, Direccion = Direccion { Calle = Calle Mayor, Ciudad = Madrid, CodigoPostal = 28002 } }

La expresión with es particularmente valiosa en escenarios como:

  • Actualización de configuraciones: Cuando necesitamos modificar solo algunos parámetros de configuración.
public record ConfiguracionApp(bool ModoOscuro, string Idioma, int TamañoFuente);

ConfiguracionApp configPredeterminada = new ConfiguracionApp(false, "es", 12);
ConfiguracionApp configUsuario = configPredeterminada with { ModoOscuro = true };
  • Flujos de trabajo inmutables: Cuando un objeto pasa por diferentes estados en un proceso.
public record Tarea(int Id, string Titulo, string Estado, DateTime FechaCreacion);

Tarea nuevaTarea = new Tarea(1, "Implementar records", "Pendiente", DateTime.Now);
Tarea tareaEnProceso = nuevaTarea with { Estado = "En progreso" };
Tarea tareaCompletada = tareaEnProceso with { Estado = "Completada" };
  • Patrones de diseño funcional: Cuando implementamos patrones funcionales que requieren inmutabilidad.
public record Carrito(List<LineaCarrito> Lineas, decimal Total);
public record LineaCarrito(string Producto, int Cantidad, decimal Precio);

// Añadir un producto al carrito creando una nueva instancia
public static Carrito AñadirProducto(Carrito carrito, string producto, int cantidad, decimal precio)
{
    var nuevasLineas = new List<LineaCarrito>(carrito.Lineas)
    {
        new LineaCarrito(producto, cantidad, precio)
    };
    
    decimal nuevoTotal = carrito.Total + (cantidad * precio);
    
    return carrito with 
    { 
        Lineas = nuevasLineas,
        Total = nuevoTotal
    };
}

La expresión with proporciona una sintaxis elegante y concisa para trabajar con datos inmutables, evitando la necesidad de crear constructores o métodos adicionales para generar copias modificadas de nuestros objetos.

Igualdad estructural

Los records en C# implementan lo que se conoce como igualdad estructural o igualdad basada en valores. Esto significa que dos instancias de un record se consideran iguales si todas sus propiedades tienen los mismos valores, a diferencia de las clases tradicionales que utilizan igualdad referencial (dos objetos son iguales solo si apuntan a la misma ubicación en memoria).

Esta característica es fundamental para trabajar con datos inmutables, ya que nos permite comparar records por su contenido en lugar de por su identidad en memoria.

Veamos cómo funciona la igualdad estructural con un ejemplo sencillo:

// Definimos un record simple
public record Persona(string Nombre, int Edad);

// Creamos dos instancias con los mismos valores
Persona persona1 = new Persona("Carlos", 30);
Persona persona2 = new Persona("Carlos", 30);

// Comprobamos si son iguales
Console.WriteLine(persona1 == persona2);  // True
Console.WriteLine(persona1.Equals(persona2));  // True
Console.WriteLine(ReferenceEquals(persona1, persona2));  // False (son objetos diferentes en memoria)

En este ejemplo, aunque persona1 y persona2 son objetos diferentes en memoria (como demuestra ReferenceEquals), se consideran iguales porque todas sus propiedades tienen los mismos valores.

La igualdad estructural también afecta a cómo funcionan los records en colecciones como diccionarios y conjuntos:

// Usando records como claves en un diccionario
Dictionary<Persona, string> departamentos = new();
departamentos[persona1] = "Desarrollo";

// Podemos acceder usando persona2 como clave
Console.WriteLine(departamentos[persona2]);  // Muestra "Desarrollo"

// Usando records en conjuntos
HashSet<Persona> equipo = new();
equipo.Add(persona1);
Console.WriteLine(equipo.Contains(persona2));  // True

La igualdad estructural también funciona con records anidados:

public record Direccion(string Calle, string Ciudad);
public record Cliente(string Nombre, Direccion Direccion);

Direccion direccion1 = new Direccion("Gran Vía", "Madrid");
Direccion direccion2 = new Direccion("Gran Vía", "Madrid");

Cliente cliente1 = new Cliente("Ana", direccion1);
Cliente cliente2 = new Cliente("Ana", direccion2);

Console.WriteLine(cliente1 == cliente2);  // True

En este caso, aunque direccion1 y direccion2 son objetos diferentes, tienen los mismos valores, por lo que cliente1 y cliente2 se consideran iguales.

La igualdad estructural es especialmente útil en escenarios como:

  • Caché y memorización: Permite identificar solicitudes idénticas.
public record SolicitudDatos(string Recurso, Dictionary<string, string> Parametros);

// Podemos usar el record como clave en un caché
Dictionary<SolicitudDatos, object> cacheDatos = new();

// Dos solicitudes con los mismos parámetros se considerarán la misma clave
  • Comparación de configuraciones: Facilita detectar si ha habido cambios.
public record Configuracion(bool ModoOscuro, string Idioma, int TamañoFuente);

Configuracion configAnterior = ObtenerConfiguracionGuardada();
Configuracion configActual = ObtenerConfiguracionActual();

if (configAnterior != configActual)
{
    // La configuración ha cambiado, guardar cambios
    GuardarConfiguracion(configActual);
}
  • Testing: Simplifica las aserciones en pruebas unitarias.
public record ResultadoOperacion(bool Exito, string Mensaje, object Datos);

// En una prueba unitaria
ResultadoOperacion resultadoEsperado = new ResultadoOperacion(true, "Operación completada", 42);
ResultadoOperacion resultadoActual = servicio.RealizarOperacion();

Assert.AreEqual(resultadoEsperado, resultadoActual);  // Compara todos los campos automáticamente

La igualdad estructural también se extiende a las colecciones dentro de los records, siempre que estas implementen la igualdad estructural:

public record Pedido(string Cliente, List<string> Productos);

List<string> productos1 = new() { "Laptop", "Mouse" };
List<string> productos2 = new() { "Laptop", "Mouse" };

Pedido pedido1 = new Pedido("Ana", productos1);
Pedido pedido2 = new Pedido("Ana", productos2);

// Esto devolverá False porque List<T> usa igualdad referencial
Console.WriteLine(pedido1 == pedido2);

Para solucionar esto, podemos usar colecciones inmutables o convertir las listas a arrays en el constructor:

public record PedidoMejorado(string Cliente, string[] Productos);

PedidoMejorado pedido1 = new PedidoMejorado("Ana", productos1.ToArray());
PedidoMejorado pedido2 = new PedidoMejorado("Ana", productos2.ToArray());

// Ahora devolverá True porque los arrays se comparan por valor
Console.WriteLine(pedido1 == pedido2);

La igualdad estructural hace que los records sean ideales para representar datos inmutables en aplicaciones, simplificando comparaciones y operaciones que serían más complejas con clases tradicionales.

Igualdad estructural

La igualdad estructural es una de las características más potentes de los records en C#. A diferencia de las clases tradicionales que utilizan igualdad referencial (dos objetos son iguales solo si apuntan a la misma dirección de memoria), los records implementan igualdad basada en valores, donde dos instancias se consideran iguales si todas sus propiedades contienen los mismos valores.

Esta característica se implementa automáticamente en los records sin necesidad de escribir código adicional. Veamos un ejemplo básico:

public record Coordenada(int X, int Y);

var punto1 = new Coordenada(10, 20);
var punto2 = new Coordenada(10, 20);

Console.WriteLine(punto1 == punto2);        // True
Console.WriteLine(punto1.Equals(punto2));   // True

Aunque punto1 y punto2 son instancias diferentes en memoria, C# las considera iguales porque sus propiedades X e Y tienen los mismos valores. Esto contrasta con el comportamiento de las clases:

public class CoordenadaClase
{
    public int X { get; init; }
    public int Y { get; init; }
    
    public CoordenadaClase(int x, int y)
    {
        X = x;
        Y = y;
    }
}

var p1 = new CoordenadaClase(10, 20);
var p2 = new CoordenadaClase(10, 20);

Console.WriteLine(p1 == p2);        // False
Console.WriteLine(p1.Equals(p2));   // False

La igualdad estructural también afecta al cálculo del código hash. Dos records con las mismas propiedades generarán el mismo valor hash:

Console.WriteLine(punto1.GetHashCode() == punto2.GetHashCode());  // True

Esta característica hace que los records sean ideales para usarse como claves en colecciones como Dictionary o HashSet:

var diccionario = new Dictionary<Coordenada, string>();
diccionario[punto1] = "Ubicación A";

// Podemos recuperar el valor usando punto2 como clave
Console.WriteLine(diccionario[punto2]);  // Muestra "Ubicación A"

La igualdad estructural también funciona con propiedades anidadas. Si un record contiene otro record como propiedad, la comparación se realizará recursivamente:

public record Tamaño(int Ancho, int Alto);
public record Rectangulo(Coordenada Posicion, Tamaño Dimension);

var rect1 = new Rectangulo(new Coordenada(5, 5), new Tamaño(10, 20));
var rect2 = new Rectangulo(new Coordenada(5, 5), new Tamaño(10, 20));

Console.WriteLine(rect1 == rect2);  // True

Es importante entender que la igualdad estructural tiene algunas consideraciones especiales con las colecciones:

public record Grupo(string Nombre, List<string> Miembros);

var lista1 = new List<string> { "Ana", "Carlos" };
var lista2 = new List<string> { "Ana", "Carlos" };

var grupo1 = new Grupo("Equipo A", lista1);
var grupo2 = new Grupo("Equipo A", lista2);

Console.WriteLine(grupo1 == grupo2);  // False

El resultado es False porque List<T> no implementa igualdad estructural. Para solucionar esto, podemos usar arrays o colecciones inmutables:

public record GrupoMejorado(string Nombre, string[] Miembros);

var grupo1 = new GrupoMejorado("Equipo A", new[] { "Ana", "Carlos" });
var grupo2 = new GrupoMejorado("Equipo A", new[] { "Ana", "Carlos" });

Console.WriteLine(grupo1 == grupo2);  // True

La igualdad estructural es especialmente útil en escenarios prácticos como:

  • Validación de datos: Comparar si un objeto ha cambiado después de una operación.
public record DatosUsuario(string Nombre, string Email, string Direccion);

var datosOriginales = new DatosUsuario("Juan", "juan@ejemplo.com", "Calle Principal 123");
var datosModificados = ObtenerDatosActualizados();

if (datosOriginales != datosModificados)
{
    // Los datos han cambiado, actualizar en la base de datos
    ActualizarBaseDeDatos(datosModificados);
}
  • Detección de duplicados: Identificar elementos repetidos en colecciones.
public record Producto(string Codigo, string Nombre);

var productos = new List<Producto>
{
    new Producto("A001", "Teclado"),
    new Producto("A002", "Mouse"),
    new Producto("A001", "Teclado")  // Duplicado
};

var productosUnicos = new HashSet<Producto>(productos);
Console.WriteLine(productosUnicos.Count);  // 2 (elimina el duplicado)
  • Memorización de resultados: Almacenar en caché resultados de operaciones basadas en parámetros.
public record ParametrosConsulta(string Tabla, int Limite, string[] Campos);

var cache = new Dictionary<ParametrosConsulta, object>();

object ObtenerDatos(ParametrosConsulta parametros)
{
    if (cache.TryGetValue(parametros, out var resultado))
        return resultado;
        
    // Realizar consulta costosa
    var datos = RealizarConsulta(parametros);
    cache[parametros] = datos;
    return datos;
}

La igualdad estructural también se puede personalizar en casos especiales mediante la sobrecarga de los métodos Equals y GetHashCode, aunque generalmente no es necesario:

public record PersonaPersonalizada(string Nombre, string Apellido)
{
    public virtual bool Equals(PersonaPersonalizada other)
    {
        // Comparar solo por nombre completo, ignorando mayúsculas/minúsculas
        return other != null && 
               string.Equals($"{Nombre} {Apellido}", 
                            $"{other.Nombre} {other.Apellido}", 
                            StringComparison.OrdinalIgnoreCase);
    }

    public override int GetHashCode()
    {
        return ($"{Nombre} {Apellido}").ToLower().GetHashCode();
    }
}

En resumen, la igualdad estructural hace que los records sean ideales para representar datos inmutables, simplificando enormemente las comparaciones y permitiendo un código más limpio y menos propenso a errores en escenarios donde la identidad de los datos está determinada por sus valores y no por su ubicación en memoria.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende CSharp online

Ejercicios de esta lección Records

Evalúa tus conocimientos de esta lección Records con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Todas las lecciones de CSharp

Accede a todas las lecciones de CSharp y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Introducción A C#

Introducción Y Entorno

Creación De Proyecto C#

Introducción Y Entorno

Variables Y Constantes

Sintaxis

Tipos De Datos

Sintaxis

Operadores

Sintaxis

Control De Flujo

Sintaxis

Funciones

Sintaxis

Estructuras De Control Iterativo

Sintaxis

Interpolación De Strings

Sintaxis

Estructuras De Control Condicional

Sintaxis

Manejo De Valores Nulos

Sintaxis

Clases Y Encapsulación

Programación Orientada A Objetos

Objetos

Programación Orientada A Objetos

Constructores Y Destructores

Programación Orientada A Objetos

Herencia

Programación Orientada A Objetos

Polimorfismo

Programación Orientada A Objetos

Genéricos

Programación Orientada A Objetos

Métodos Virtuales Y Sobrecarga

Programación Orientada A Objetos

Clases Abstractas

Programación Orientada A Objetos

Interfaces

Programación Orientada A Objetos

Propiedades Y Encapsulación

Programación Orientada A Objetos

Métodos De Extensión

Programación Orientada A Objetos

Clases Y Objetos

Programación Orientada A Objetos

Clases Parciales

Programación Orientada A Objetos

Miembros Estáticos

Programación Orientada A Objetos

Tuplas Y Tipos Anónimos

Programación Orientada A Objetos

Arrays Y Listas

Colecciones Y Linq

Diccionarios

Colecciones Y Linq

Conjuntos, Colas Y Pilas

Colecciones Y Linq

Uso De Consultas Linq

Colecciones Y Linq

Linq Avanzado

Colecciones Y Linq

Colas Y Pilas

Colecciones Y Linq

Conjuntos

Colecciones Y Linq

Linq Básico

Colecciones Y Linq

Delegados Funcionales

Programación Funcional

Records

Programación Funcional

Expresiones Lambda

Programación Funcional

Linq Funcional

Programación Funcional

Fundamentos De La Programación Funcional

Programación Funcional

Pattern Matching

Programación Funcional

Testing Unitario Con Xunit

Testing

Excepciones

Excepciones

Delegados

Programación Asíncrona

Eventos

Programación Asíncrona

Lambdas

Programación Asíncrona

Uso De Async Y Await

Programación Asíncrona

Tareas

Programación Asíncrona

Accede GRATIS a CSharp y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender qué son los records y cómo declararlos en C#.
  • Aprender a utilizar el operador with para crear copias modificadas de records inmutables.
  • Entender la igualdad estructural y cómo afecta a la comparación de objetos record.
  • Conocer las ventajas de los records frente a las clases tradicionales en escenarios de datos inmutables.
  • Aplicar records en ejemplos prácticos como DTOs, configuraciones y modelos de dominio.