Ir al contenido principal

Pattern matching

El pattern matching es una característica poderosa y expresiva de C# que permite examinar datos y ejecutar código basado en la forma, estructura o contenido de esos datos. Esta funcionalidad ha evolucionado significativamente desde su introducción, transformando la manera en que podemos escribir código más legible y conciso para la toma de decisiones complejas.

A diferencia de las estructuras de control tradicionales que se limitan a comparaciones simples, el pattern matching nos permite realizar coincidencias sofisticadas contra tipos, valores, propiedades y estructuras de datos. Esta capacidad es especialmente útil cuando trabajamos con jerarquías de tipos, análisis de datos complejos o cuando necesitamos implementar lógica de negocio que dependa de múltiples condiciones.

En este artículo exploraremos las diferentes formas de pattern matching disponibles en C#, desde los patrones básicos hasta las características más avanzadas introducidas en las versiones recientes del lenguaje, aprendiendo cómo aplicarlas para escribir código más expresivo y mantenible.

Fundamentos del pattern matching

El pattern matching en C# se basa en el concepto de patrones que describen la forma o características que deben cumplir los datos para que se ejecute un bloque de código específico. Un patrón puede coincidir con tipos, valores constantes, propiedades de objetos, o combinaciones más complejas.

Elementos básicos de los patrones

Tipo de patrón Descripción Ejemplo
Patrón de tipo Coincide con un tipo específico obj is string
Patrón constante Coincide con un valor específico valor is 42
Patrón var Siempre coincide y captura el valor obj is var x
Patrón de descarte Coincide pero ignora el valor obj is _

La sintaxis básica para pattern matching utiliza principalmente dos construcciones:

using System;

class Program
{
    static void Main()
    {
        object[] elementos = { 42, "Hola", 3.14, true, null };
        
        foreach (var elemento in elementos)
        {
            // Uso del operador 'is' con patrones
            if (elemento is int numero)
            {
                Console.WriteLine($"Número entero: {numero}");
            }
            else if (elemento is string texto)
            {
                Console.WriteLine($"Texto: {texto}");
            }
            else if (elemento is double decimal_)
            {
                Console.WriteLine($"Número decimal: {decimal_}");
            }
            else if (elemento is bool booleano)
            {
                Console.WriteLine($"Valor booleano: {booleano}");
            }
            else if (elemento is null)
            {
                Console.WriteLine("Valor nulo encontrado");
            }
        }
    }
}

Expresiones switch tradicionales vs pattern matching

Las expresiones switch han evolucionado para soportar pattern matching, ofreciendo mayor flexibilidad:

using System;

class Program
{
    static void Main()
    {
        ProcesarDatos(25);
        ProcesarDatos("Bienvenido");
        ProcesarDatos(3.14159);
        ProcesarDatos(new int[] { 1, 2, 3 });
    }
    
    static void ProcesarDatos(object dato)
    {
        // Switch tradicional mejorado con patrones
        switch (dato)
        {
            case int i when i > 0:
                Console.WriteLine($"Número entero positivo: {i}");
                break;
            case int i when i < 0:
                Console.WriteLine($"Número entero negativo: {i}");
                break;
            case int i:
                Console.WriteLine($"Número entero cero: {i}");
                break;
            case string s when s.Length > 10:
                Console.WriteLine($"Texto largo: {s}");
                break;
            case string s:
                Console.WriteLine($"Texto corto: {s}");
                break;
            case double d:
                Console.WriteLine($"Número decimal: {d:F2}");
                break;
            case int[] arreglo:
                Console.WriteLine($"Arreglo de {arreglo.Length} elementos");
                break;
            case null:
                Console.WriteLine("Valor nulo");
                break;
            default:
                Console.WriteLine($"Tipo no reconocido: {dato.GetType().Name}");
                break;
        }
    }
}

Patrones con guardas (when)

Las cláusulas when permiten añadir condiciones adicionales a los patrones, proporcionando mayor precisión en las coincidencias:

using System;

class Producto
{
    public string Nombre { get; set; }
    public decimal Precio { get; set; }
    public string Categoria { get; set; }
    public int Stock { get; set; }
}

class Program
{
    static void Main()
    {
        var productos = new[]
        {
            new Producto { Nombre = "Laptop", Precio = 1200m, Categoria = "Electrónicos", Stock = 5 },
            new Producto { Nombre = "Libro", Precio = 15m, Categoria = "Educación", Stock = 0 },
            new Producto { Nombre = "Auriculares", Precio = 80m, Categoria = "Electrónicos", Stock = 20 }
        };
        
        foreach (var producto in productos)
        {
            string estado = EvaluarProducto(producto);
            Console.WriteLine($"{producto.Nombre}: {estado}");
        }
    }
    
    static string EvaluarProducto(Producto producto)
    {
        return producto switch
        {
            // Patrones con múltiples condiciones
            { Stock: 0 } => "Producto agotado",
            { Precio: var p } when p > 1000 => "Producto premium",
            { Categoria: "Electrónicos", Stock: var s } when s < 10 => "Electrónico con poco stock",
            { Categoria: "Electrónicos" } => "Electrónico disponible",
            { Precio: var p } when p < 20 => "Producto económico",
            _ => "Producto estándar"
        };
    }
}

Patrones de propiedades

Los patrones de propiedades permiten hacer coincidencias basadas en los valores de las propiedades de un objeto:

using System;

class Empleado
{
    public string Nombre { get; set; }
    public string Departamento { get; set; }
    public int Antiguedad { get; set; }
    public decimal Salario { get; set; }
    public bool EsGerente { get; set; }
}

class Program
{
    static void Main()
    {
        var empleados = new[]
        {
            new Empleado { Nombre = "Ana", Departamento = "IT", Antiguedad = 8, Salario = 55000, EsGerente = true },
            new Empleado { Nombre = "Luis", Departamento = "Ventas", Antiguedad = 3, Salario = 35000, EsGerente = false },
            new Empleado { Nombre = "María", Departamento = "IT", Antiguedad = 15, Salario = 70000, EsGerente = false }
        };
        
        foreach (var empleado in empleados)
        {
            string categoria = CategoriaEmpleado(empleado);
            Console.WriteLine($"{empleado.Nombre} - {categoria}");
        }
    }
    
    static string CategoriaEmpleado(Empleado empleado)
    {
        return empleado switch
        {
            // Patrón de propiedades específicas
            { EsGerente: true } => "Gerencia",
            
            // Patrones con múltiples propiedades
            { Departamento: "IT", Antiguedad: >= 10 } => "Senior IT",
            { Departamento: "IT", Antiguedad: >= 5 } => "Mid-level IT",
            { Departamento: "IT" } => "Junior IT",
            
            // Patrones con propiedades anidadas y condiciones
            { Salario: >= 60000, Antiguedad: >= 10 } => "Empleado veterano",
            { Salario: >= 40000, Antiguedad: >= 5 } => "Empleado experimentado",
            
            // Patrón por defecto
            _ => "Empleado estándar"
        };
    }
}

Patrones de propiedades anidadas

Para objetos con estructuras más complejas, podemos usar patrones de propiedades anidadas:

using System;

class Direccion
{
    public string Ciudad { get; set; }
    public string CodigoPostal { get; set; }
    public string Pais { get; set; }
}

class Cliente
{
    public string Nombre { get; set; }
    public int Edad { get; set; }
    public Direccion Direccion { get; set; }
    public decimal VolumenCompras { get; set; }
}

class Program
{
    static void Main()
    {
        var clientes = new[]
        {
            new Cliente 
            { 
                Nombre = "Carlos", 
                Edad = 35, 
                Direccion = new Direccion { Ciudad = "Madrid", Pais = "España" },
                VolumenCompras = 15000
            },
            new Cliente 
            { 
                Nombre = "Sophie", 
                Edad = 28, 
                Direccion = new Direccion { Ciudad = "París", Pais = "Francia" },
                VolumenCompras = 5000
            }
        };
        
        foreach (var cliente in clientes)
        {
            string tipoCliente = ClasificarCliente(cliente);
            Console.WriteLine($"{cliente.Nombre}: {tipoCliente}");
        }
    }
    
    static string ClasificarCliente(Cliente cliente)
    {
        return cliente switch
        {
            // Patrones de propiedades anidadas
            { Direccion: { Pais: "España", Ciudad: "Madrid" }, VolumenCompras: >= 10000 } => "Cliente VIP Madrid",
            { Direccion: { Pais: "España" }, VolumenCompras: >= 5000 } => "Cliente premium España",
            { Direccion: { Pais: "España" } } => "Cliente España",
            
            // Patrones con múltiples niveles
            { Edad: >= 65, Direccion: { Pais: var pais } } => $"Cliente senior en {pais}",
            { Edad: <= 25, VolumenCompras: <= 1000 } => "Cliente joven principiante",
            
            _ => "Cliente estándar internacional"
        };
    }
}

Patrones de tuplas

Los patrones de tuplas permiten hacer coincidencias con múltiples valores simultáneamente:

using System;

class Program
{
    static void Main()
    {
        // Ejemplos de clasificación basada en múltiples valores
        Console.WriteLine(ClasificarTemperatura(25, "Soleado"));
        Console.WriteLine(ClasificarTemperatura(5, "Nublado"));
        Console.WriteLine(ClasificarTemperatura(35, "Lluvioso"));
        
        // Análisis de coordenadas
        Console.WriteLine(AnalizarCoordenadas(0, 0));
        Console.WriteLine(AnalizarCoordenadas(5, 3));
        Console.WriteLine(AnalizarCoordenadas(-2, 8));
        
        // Evaluación de rendimiento
        Console.WriteLine(EvaluarRendimiento(95, 8));
        Console.WriteLine(EvaluarRendimiento(60, 12));
    }
    
    static string ClasificarTemperatura(int temperatura, string clima)
    {
        return (temperatura, clima) switch
        {
            (>= 30, "Soleado") => "Día perfecto para la playa",
            (>= 20, "Soleado") => "Buen día para actividades exteriores",
            (< 10, "Lluvioso") => "Día para quedarse en casa",
            (>= 25, "Lluvioso") => "Lluvia tropical",
            (< 0, _) => "Cuidado con el hielo",
            (_, "Nublado") => "Día tranquilo",
            _ => "Clima normal"
        };
    }
    
    static string AnalizarCoordenadas(int x, int y)
    {
        return (x, y) switch
        {
            (0, 0) => "Origen del sistema de coordenadas",
            (var a, 0) when a > 0 => "Eje X positivo",
            (var a, 0) when a < 0 => "Eje X negativo",
            (0, var b) when b > 0 => "Eje Y positivo",
            (0, var b) when b < 0 => "Eje Y negativo",
            (var a, var b) when a > 0 && b > 0 => "Primer cuadrante",
            (var a, var b) when a < 0 && b > 0 => "Segundo cuadrante",
            (var a, var b) when a < 0 && b < 0 => "Tercer cuadrante",
            _ => "Cuarto cuadrante"
        };
    }
    
    static string EvaluarRendimiento(int puntuacion, int horasTrabajadas)
    {
        return (puntuacion, horasTrabajadas) switch
        {
            (>= 90, <= 8) => "Rendimiento excepcional y eficiente",
            (>= 90, _) => "Alto rendimiento",
            (>= 70, <= 8) => "Buen rendimiento con eficiencia",
            (>= 70, _) => "Rendimiento aceptable",
            (_, >= 12) => "Mucho esfuerzo, revisar eficiencia",
            _ => "Rendimiento por debajo del estándar"
        };
    }
}

Patrones posicionales y deconstrucción

Los patrones posicionales trabajan con la deconstrucción de objetos, permitiendo extraer valores de manera estructurada:

using System;

// Clase que soporta deconstrucción
class Punto
{
    public double X { get; }
    public double Y { get; }
    
    public Punto(double x, double y) => (X, Y) = (x, y);
    
    // Método de deconstrucción
    public void Deconstruct(out double x, out double y) => (x, y) = (X, Y);
}

class Rectangulo
{
    public Punto Esquina1 { get; }
    public Punto Esquina2 { get; }
    
    public Rectangulo(Punto p1, Punto p2) => (Esquina1, Esquina2) = (p1, p2);
    
    public void Deconstruct(out Punto p1, out Punto p2) => (p1, p2) = (Esquina1, Esquina2);
}

class Program
{
    static void Main()
    {
        var puntos = new[]
        {
            new Punto(0, 0),
            new Punto(1, 1),
            new Punto(-5, 3),
            new Punto(0, -2)
        };
        
        foreach (var punto in puntos)
        {
            string descripcion = DescribirPunto(punto);
            Console.WriteLine($"Punto ({punto.X}, {punto.Y}): {descripcion}");
        }
        
        var rectangulo = new Rectangulo(new Punto(0, 0), new Punto(5, 3));
        string tipoRectangulo = AnalizarRectangulo(rectangulo);
        Console.WriteLine($"Rectángulo: {tipoRectangulo}");
    }
    
    static string DescribirPunto(Punto punto)
    {
        return punto switch
        {
            // Patrón posicional con deconstrucción
            (0, 0) => "Origen",
            (var x, 0) when x > 0 => "Eje X positivo",
            (var x, 0) when x < 0 => "Eje X negativo",
            (0, var y) when y > 0 => "Eje Y positivo",
            (0, var y) when y < 0 => "Eje Y negativo",
            (var x, var y) when x == y => "Diagonal principal",
            (var x, var y) when x == -y => "Diagonal secundaria",
            (var x, var y) when Math.Abs(x) == Math.Abs(y) => "Punto simétrico",
            _ => "Punto general"
        };
    }
    
    static string AnalizarRectangulo(Rectangulo rect)
    {
        return rect switch
        {
            // Deconstrucción anidada
            ((0, 0), var (x2, y2)) when x2 == y2 => "Cuadrado desde origen",
            ((0, 0), _) => "Rectángulo desde origen",
            (var (x1, y1), var (x2, y2)) when Math.Abs(x2 - x1) == Math.Abs(y2 - y1) => "Cuadrado",
            _ => "Rectángulo general"
        };
    }
}

Expresiones switch avanzadas

Las expresiones switch (introducidas en C# 8.0) proporcionan una sintaxis más concisa y funcional:

using System;
using System.Collections.Generic;

enum TipoVehiculo { Coche, Moto, Camion, Bicicleta }
enum EstadoVehiculo { Nuevo, Usado, Dañado }

class Vehiculo
{
    public TipoVehiculo Tipo { get; set; }
    public EstadoVehiculo Estado { get; set; }
    public int Año { get; set; }
    public decimal PrecioBase { get; set; }
}

class Program
{
    static void Main()
    {
        var vehiculos = new[]
        {
            new Vehiculo { Tipo = TipoVehiculo.Coche, Estado = EstadoVehiculo.Nuevo, Año = 2023, PrecioBase = 25000 },
            new Vehiculo { Tipo = TipoVehiculo.Moto, Estado = EstadoVehiculo.Usado, Año = 2020, PrecioBase = 8000 },
            new Vehiculo { Tipo = TipoVehiculo.Camion, Estado = EstadoVehiculo.Dañado, Año = 2018, PrecioBase = 45000 }
        };
        
        foreach (var vehiculo in vehiculos)
        {
            decimal precio = CalcularPrecio(vehiculo);
            string categoria = CategoriaVehiculo(vehiculo);
            decimal impuesto = CalcularImpuesto(vehiculo);
            
            Console.WriteLine($"{vehiculo.Tipo} {vehiculo.Estado} ({vehiculo.Año})");
            Console.WriteLine($"  Categoría: {categoria}");
            Console.WriteLine($"  Precio: {precio:C}");
            Console.WriteLine($"  Impuesto: {impuesto:C}");
            Console.WriteLine();
        }
    }
    
    static decimal CalcularPrecio(Vehiculo vehiculo) => vehiculo switch
    {
        // Expresiones switch con patrones de propiedades
        { Estado: EstadoVehiculo.Nuevo } => vehiculo.PrecioBase,
        { Estado: EstadoVehiculo.Usado, Año: >= 2020 } => vehiculo.PrecioBase * 0.8m,
        { Estado: EstadoVehiculo.Usado, Año: >= 2015 } => vehiculo.PrecioBase * 0.6m,
        { Estado: EstadoVehiculo.Usado } => vehiculo.PrecioBase * 0.4m,
        { Estado: EstadoVehiculo.Dañado } => vehiculo.PrecioBase * 0.2m,
        _ => vehiculo.PrecioBase
    };
    
    static string CategoriaVehiculo(Vehiculo vehiculo) => (vehiculo.Tipo, vehiculo.Estado) switch
    {
        // Patrones de tuplas con enums
        (TipoVehiculo.Coche, EstadoVehiculo.Nuevo) => "Vehículo premium",
        (TipoVehiculo.Coche, _) => "Automóvil",
        (TipoVehiculo.Moto, EstadoVehiculo.Nuevo) => "Motocicleta premium",
        (TipoVehiculo.Moto, _) => "Motocicleta",
        (TipoVehiculo.Camion, _) => "Vehículo comercial",
        (TipoVehiculo.Bicicleta, _) => "Vehículo ecológico",
        _ => "Vehículo estándar"
    };
    
    static decimal CalcularImpuesto(Vehiculo vehiculo) => vehiculo switch
    {
        // Combinación de múltiples patrones
        { Tipo: TipoVehiculo.Bicicleta } => 0m,
        { Tipo: TipoVehiculo.Moto, Estado: EstadoVehiculo.Nuevo } => 500m,
        { Tipo: TipoVehiculo.Moto } => 250m,
        { Tipo: TipoVehiculo.Coche, PrecioBase: >= 30000 } => 2000m,
        { Tipo: TipoVehiculo.Coche } => 1000m,
        { Tipo: TipoVehiculo.Camion } => 3000m,
        _ => 0m
    };
}

Casos de uso prácticos

Procesamiento de datos JSON-like

using System;
using System.Collections.Generic;

abstract class JsonValue { }
class JsonString : JsonValue { public string Value { get; set; } }
class JsonNumber : JsonValue { public decimal Value { get; set; } }
class JsonBool : JsonValue { public bool Value { get; set; } }
class JsonArray : JsonValue { public List<JsonValue> Values { get; set; } }
class JsonObject : JsonValue { public Dictionary<string, JsonValue> Properties { get; set; } }
class JsonNull : JsonValue { }

class JsonProcessor
{
    public string ConvertirATexto(JsonValue valor) => valor switch
    {
        JsonString { Value: var s } => $"\"{s}\"",
        JsonNumber { Value: var n } => n.ToString(),
        JsonBool { Value: var b } => b.ToString().ToLower(),
        JsonNull => "null",
        JsonArray { Values: var arr } => "[" + string.Join(", ", arr.ConvertAll(ConvertirATexto)) + "]",
        JsonObject { Properties: var props } => ProcessObject(props),
        _ => "unknown"
    };
    
    public int ContarElementos(JsonValue valor) => valor switch
    {
        JsonArray { Values: var arr } => arr.Count,
        JsonObject { Properties: var props } => props.Count,
        JsonNull => 0,
        _ => 1
    };
    
    public bool EsComplejo(JsonValue valor) => valor switch
    {
        JsonArray { Values: var arr } when arr.Count > 5 => true,
        JsonObject { Properties: var props } when props.Count > 3 => true,
        JsonArray { Values: var arr } => arr.Exists(v => EsComplejo(v)),
        JsonObject { Properties: var props } => props.Values.Any(v => EsComplejo(v)),
        _ => false
    };
    
    private string ProcessObject(Dictionary<string, JsonValue> props)
    {
        var pairs = new List<string>();
        foreach (var kvp in props)
        {
            pairs.Add($"\"{kvp.Key}\": {ConvertirATexto(kvp.Value)}");
        }
        return "{" + string.Join(", ", pairs) + "}";
    }
}

class Program
{
    static void Main()
    {
        var processor = new JsonProcessor();
        
        var datos = new JsonObject
        {
            Properties = new Dictionary<string, JsonValue>
            {
                ["nombre"] = new JsonString { Value = "Juan" },
                ["edad"] = new JsonNumber { Value = 30 },
                ["activo"] = new JsonBool { Value = true },
                ["hobbies"] = new JsonArray 
                { 
                    Values = new List<JsonValue>
                    {
                        new JsonString { Value = "programar" },
                        new JsonString { Value = "leer" }
                    }
                }
            }
        };
        
        Console.WriteLine($"JSON: {processor.ConvertirATexto(datos)}");
        Console.WriteLine($"Elementos: {processor.ContarElementos(datos)}");
        Console.WriteLine($"Es complejo: {processor.EsComplejo(datos)}");
    }
}

Sistema de validación flexible

using System;

abstract class ResultadoValidacion { }
class Valido : ResultadoValidacion { }
class ErrorValidacion : ResultadoValidacion 
{ 
    public string Mensaje { get; set; }
    public ErrorValidacion(string mensaje) => Mensaje = mensaje;
}

class ValidadorFormulario
{
    public ResultadoValidacion ValidarEmail(string email) => email switch
    {
        null or "" => new ErrorValidacion("El email es obligatorio"),
        var e when !e.Contains("@") => new ErrorValidacion("Email sin formato válido"),
        var e when e.Length > 100 => new ErrorValidacion("Email demasiado largo"),
        var e when e.StartsWith("admin@") => new ErrorValidacion("No se permiten emails de administrador"),
        _ => new Valido()
    };
    
    public ResultadoValidacion ValidarEdad(int? edad) => edad switch
    {
        null => new ErrorValidacion("La edad es obligatoria"),
        < 0 => new ErrorValidacion("La edad no puede ser negativa"),
        > 120 => new ErrorValidacion("La edad no puede ser mayor a 120 años"),
        < 18 => new ErrorValidacion("Debe ser mayor de edad"),
        _ => new Valido()
    };
    
    public string ProcesarResultado(ResultadoValidacion resultado) => resultado switch
    {
        Valido => "✓ Validación exitosa",
        ErrorValidacion { Mensaje: var msg } => $"✗ Error: {msg}",
        _ => "Estado desconocido"
    };
}

class Program
{
    static void Main()
    {
        var validador = new ValidadorFormulario();
        
        string[] emails = { "juan@email.com", "", "invalido", "admin@sistema.com", "usuario@dominio.co" };
        int?[] edades = { 25, null, -5, 150, 16, 30 };
        
        Console.WriteLine("=== Validación de emails ===");
        foreach (var email in emails)
        {
            var resultado = validador.ValidarEmail(email);
            Console.WriteLine($"'{email}': {validador.ProcesarResultado(resultado)}");
        }
        
        Console.WriteLine("\n=== Validación de edades ===");
        foreach (var edad in edades)
        {
            var resultado = validador.ValidarEdad(edad);
            Console.WriteLine($"{edad?.ToString() ?? "null"}: {validador.ProcesarResultado(resultado)}");
        }
    }
}

Resumen

El pattern matching en C# representa una evolución significativa en la expresividad del lenguaje, proporcionando herramientas poderosas para la toma de decisiones basada en datos complejos. Desde los patrones básicos con el operador is hasta las expresiones switch avanzadas, estas características nos permiten escribir código más legible y conciso para manejar lógica condicional compleja.

Las diferentes formas de pattern matching —patrones de tipo, propiedades, tuplas, posicionales y con guardas— ofrecen flexibilidad para abordar diversos escenarios de programación. La capacidad de combinar múltiples patrones y usar deconstrucción hace que el código sea más expresivo y fácil de mantener. Estas herramientas son especialmente valiosas en aplicaciones que manejan datos heterogéneos, implementan máquinas de estado, o requieren validaciones complejas, transformando operaciones que tradicionalmente requerían múltiples estructuras if-else en expresiones elegantes y declarativas.