En esta página
Records y pattern matching en C# 14
¿Qué son los records?
Los records son tipos de referencia (o valor) diseñados para modelar datos inmutables. A diferencia de las clases, los records tienen:
- Igualdad por valor — dos records con los mismos valores son iguales
- Deconstrucción automática
- ToString() descriptivo generado automáticamente
- Expresión with para clonar con cambios
// Clase: igualdad por referencia
class PersonaClase { public string Nombre { get; set; } = ""; }
var c1 = new PersonaClase { Nombre = "David" };
var c2 = new PersonaClase { Nombre = "David" };
Console.WriteLine(c1 == c2); // false — referencias distintas
// Record: igualdad por valor
record PersonaRecord(string Nombre, int Edad);
var r1 = new PersonaRecord("David", 30);
var r2 = new PersonaRecord("David", 30);
Console.WriteLine(r1 == r2); // true — mismos valoresRecords posicionales
La forma más concisa. Los parámetros del constructor primario se convierten en propiedades init-only:
// Sintaxis posicional
record Coordenada(double Latitud, double Longitud);
var bogota = new Coordenada(-4.53, -75.67);
Console.WriteLine(bogota); // Coordenada { Latitud = -4.53, Longitud = -75.67 }
// Deconstrucción
var (lat, lon) = bogota;
Console.WriteLine($"Lat: {lat}, Lon: {lon}");
// LINQ con records
var ciudades = new List<Coordenada>
{
new(-4.53, -75.67),
new(-16.50, -68.15),
new(-17.39, -66.16),
};
var cercaDelEcuador = ciudades.Where(c => Math.Abs(c.Latitud) < 10);Expresión with
Crea una copia del record con propiedades modificadas:
record Pedido(int Id, string Cliente, decimal Total, string Estado);
var pedido = new Pedido(1, "David", 299.99m, "pendiente");
// Cambiar el estado sin mutar el original
var pedidoAprobado = pedido with { Estado = "aprobado" };
var pedidoCancelado = pedido with { Estado = "cancelado", Total = 0m };
Console.WriteLine(pedido.Estado); // pendiente — intacto
Console.WriteLine(pedidoAprobado.Estado); // aprobado
// Útil en cadenas de transformación
var pedidoFinal = pedido
with { Estado = "procesando" }
with { Total = pedido.Total * 0.9m }; // descuento 10%record struct
Para tipos pequeños que se copian frecuentemente, record struct es más eficiente al vivir en el stack:
// record struct — semántica de valor (se copia, no se comparte referencia)
record struct Color(byte R, byte G, byte B, byte A = 255)
{
public static readonly Color Rojo = new(255, 0, 0);
public static readonly Color Verde = new(0, 255, 0);
public static readonly Color Azul = new(0, 0, 255);
public static readonly Color Blanco = new(255, 255, 255);
// Método de instancia
public Color Mezclar(Color otro) =>
new((byte)((R + otro.R) / 2),
(byte)((G + otro.G) / 2),
(byte)((B + otro.B) / 2));
public string ToHex() => $"#{R:X2}{G:X2}{B:X2}";
}
var morado = Color.Rojo.Mezclar(Color.Azul);
Console.WriteLine(morado.ToHex()); // #7F007FPattern Matching
El pattern matching permite comprobar la forma de un valor y extraer información de forma declarativa.
is expression
object obj = "Hola .NET";
// Type pattern
if (obj is string s)
Console.WriteLine($"Es un string de {s.Length} caracteres");
// Null check
if (obj is not null)
Console.WriteLine("No es null");
// Constant pattern
if (obj is "Hola .NET")
Console.WriteLine("Match exacto");switch expression avanzado
// Property patterns
record Empleado(string Nombre, string Rol, decimal Salario, bool Activo);
static decimal CalcularBonus(Empleado emp) => emp switch
{
{ Activo: false } => 0,
{ Rol: "CEO" } => emp.Salario * 0.5m,
{ Rol: "Manager", Salario: > 5000 } => emp.Salario * 0.2m,
{ Rol: "Developer", Salario: > 3000 } => emp.Salario * 0.15m,
{ Activo: true } => emp.Salario * 0.05m,
_ => 0
};
var dev = new Empleado("David", "Developer", 4000m, true);
Console.WriteLine($"Bonus: ${CalcularBonus(dev):F2}"); // $600.00Patrones relacionales y lógicos
// Relacionales: <, >, <=, >=
static string ClasificarIMC(double imc) => imc switch
{
< 18.5 => "Bajo peso",
>= 18.5 and < 25.0 => "Normal",
>= 25.0 and < 30.0 => "Sobrepeso",
>= 30.0 => "Obesidad"
};
// Lógicos: and, or, not
static bool EsHorarioLaboral(int hora) =>
hora is (>= 8 and <= 12) or (>= 14 and <= 18);
static bool EsDiaLaboral(DayOfWeek dia) =>
dia is not (DayOfWeek.Saturday or DayOfWeek.Sunday);List patterns (C# 11+)
// Emparejar listas por estructura
static string AnalizarSecuencia(int[] seq) => seq switch
{
[] => "vacía",
[0] => "solo cero",
[_, 0, _] => "cero en el medio",
[> 0, > 0, > 0] => "todos positivos (3 elementos)",
[var a, var b] when a == b => $"dos iguales: {a}",
[var h, .. var rest] => $"inicia con {h}, {rest.Length} más"
};
Console.WriteLine(AnalizarSecuencia(new[] { 5, 3, 8, 1 }));
// "inicia con 5, 3 más"Deconstruct en patrones
record Punto(double X, double Y);
static string UbicacionPunto(Punto p) => p switch
{
(0, 0) => "Origen",
(> 0, > 0) => "Cuadrante I",
(< 0, > 0) => "Cuadrante II",
(< 0, < 0) => "Cuadrante III",
(> 0, < 0) => "Cuadrante IV",
(_, 0) or (0, _) => "Sobre un eje"
};
var p = new Punto(3, -1);
Console.WriteLine(UbicacionPunto(p)); // Cuadrante IVPráctica
- Records de dominio: Crea
record Factura(int Id, string Cliente, List<LineaFactura> Items)yrecord LineaFactura(string Producto, decimal Precio, int Cantidad). Usa with para marcar una factura como "pagada". - Pattern matching de formas: Implementa
DescribirForma(object forma)que use switch expression con type patterns paraCirculo,RectanguloyTriangulo. - List patterns: Escribe un método que analice los primeros 3 elementos de una lista de precios y devuelva "Tendencia alcista", "Tendencia bajista" o "Estable".
En la siguiente lección comenzaremos con ASP.NET Core 10: cómo construir APIs web con Minimal APIs y el pipeline de middleware.
// Record class — inmutable por convención, value equality
public record Producto(int Id, string Nombre, decimal Precio);
// Posicional: deconstrucción automática
var laptop = new Producto(1, "Laptop Pro", 1299.99m);
var (id, nombre, precio) = laptop;
Console.WriteLine($"#{id}: {nombre} — ${precio:F2}");
// with expression — clonar con cambios (NO muta el original)
var laptopDescuento = laptop with { Precio = 999.99m };
Console.WriteLine(laptop.Precio); // 1299.99 — sin cambios
Console.WriteLine(laptopDescuento.Precio); // 999.99
// Value equality — comparación por valor (no por referencia)
var p1 = new Producto(1, "Mouse", 29.99m);
var p2 = new Producto(1, "Mouse", 29.99m);
Console.WriteLine(p1 == p2); // true (records)
Console.WriteLine(ReferenceEquals(p1, p2)); // false
// Record struct — valor en stack, más eficiente en memoria
public record struct Punto(double X, double Y);
var origen = new Punto(0, 0);
var p = new Punto(3.0, 4.0);
Console.WriteLine(p.ToString()); // Punto { X = 3, Y = 4 }
// Record con propiedades adicionales y método
public record Usuario(int Id, string Email)
{
public string Nombre { get; init; } = string.Empty;
public DateTime CreadoEn { get; init; } = DateTime.UtcNow;
public string Inicial => Nombre.Length > 0 ? Nombre[0].ToString() : "?";
}
Inicia sesión para guardar tu progreso