22 mayo 2025

Coincidencia de Patrones en C#

C# moderno ha experimentado una revolución silenciosa en la forma en que manejamos la lógica condicional. Atrás quedaron los días en que las comprobaciones de tipo y las comparaciones de valores requerían cascadas if-else prolijas o sentencias switch torpes. La introducción de sofisticadas capacidades de coincidencia de patrones, particularmente desde C# 8.0, ha cambiado fundamentalmente la forma en que los desarrolladores escriben el flujo de control, haciendo que el código sea simultáneamente más expresivo, más conciso y más seguro.

Mejorando el Flujo de Control con la Coincidencia de Patrones en C#

La coincidencia de patrones en C# ofrece una sintaxis concisa para probar una expresión y realizar acciones cuando esa expresión coincide con un patrón específico. Permite a los desarrolladores probar valores contra varios criterios, incluyendo su tipo, valor, propiedades o incluso la estructura de objetos complejos. Esta capacidad se expone principalmente a través de la expresión is y la expresión switch (o sentencia switch). Si bien la comprobación básica de tipos ya existía, C# 8.0 introdujo un vocabulario más rico de patrones, extendiendo significativamente su utilidad e impacto en las prácticas de codificación diarias. Esta mejora es crucial para una coincidencia de patrones eficaz en switch de C# y en if de C#, transformando la lógica condicional prolija en construcciones compactas y comprensibles.

Exploremos algunos de los tipos de patrones clave disponibles y cómo simplifican las tareas de programación comunes, proporcionando ejemplos claros de coincidencia de patrones en C# que ilustran sus beneficios.

Ejemplos Prácticos de Coincidencia de Patrones en C#

El verdadero poder de la coincidencia de patrones se hace evidente al contrastarla con los enfoques tradicionales. Los desarrolladores obtienen mejoras significativas en la legibilidad y concisión del código.

Patrón de Declaración

El patrón de declaración verifica el tipo en tiempo de ejecución de una expresión y, si coincide, asigna el resultado a una nueva variable. Esto elimina la necesidad de conversiones de tipo explícitas y comprobaciones de null en muchos escenarios.

Antes:

public void ProcessAssetOld(object asset)
{
    if (asset is SoftwareLicense)
    {
        SoftwareLicense license = (SoftwareLicense)asset;
        Console.WriteLine($"Software License: {license.ProductName}, Expiration: {license.ExpirationDate.ToShortDateString()}");
    }
    else if (asset is HardwareComponent)
    {
        HardwareComponent hardware = (HardwareComponent)asset;
        Console.WriteLine($"Hardware Component: {hardware.ComponentName}, Serial: {hardware.SerialNumber}");
    }
    else
    {
        Console.WriteLine("Unknown asset type.");
    }
}

public class SoftwareLicense { public string ProductName { get; set; } public DateTime ExpirationDate { get; set; } }
public class HardwareComponent { public string ComponentName { get; set; } public string SerialNumber { get; set; } }

Después (usando el Patrón de Declaración):

public void ProcessAssetNew(object asset)
{
    if (asset is SoftwareLicense license)
    {
        Console.WriteLine($"Software License: {license.ProductName}, Expiration: {license.ExpirationDate.ToShortDateString()}");
    }
    else if (asset is HardwareComponent hardware)
    {
        Console.WriteLine($"Hardware Component: {hardware.ComponentName}, Serial: {hardware.SerialNumber}");
    }
    else
    {
        Console.WriteLine("Unknown asset type.");
    }
}

Las variables license y hardware solo están en el ámbito y se asignan si el patrón coincide, mejorando la seguridad de tipos y reduciendo posibles errores en tiempo de ejecución.

Patrón de Tipo

Similar al patrón de declaración, el patrón de tipo verifica el tipo en tiempo de ejecución de una expresión. A menudo se usa dentro de expresiones switch donde una nueva declaración de variable no es estrictamente necesaria para el patrón en sí.

Antes:

public decimal CalculateShippingCostOld(Shipment shipment)
{
    if (shipment == null)
        throw new ArgumentNullException(nameof(shipment));
    if (shipment.GetType() == typeof(StandardShipment))
        return 5.00m;
    if (shipment.GetType() == typeof(ExpressShipment))
        return 15.00m;
    if (shipment.GetType() == typeof(InternationalShipment))
        return 25.00m;
    return 0.00m; // Default for unknown types
}

public abstract class Shipment { }
public class StandardShipment : Shipment { }
public class ExpressShipment : Shipment { }
public class InternationalShipment : Shipment { }

Después (usando el Patrón de Tipo en la expresión switch):

public decimal CalculateShippingCostNew(Shipment shipment) => shipment switch
{
    StandardShipment => 5.00m,
    ExpressShipment => 15.00m,
    InternationalShipment => 25.00m,
    null => throw new ArgumentNullException(nameof(shipment)),
    _ => 0.00m // Handle unknown shipment types
};

Esto demuestra una coincidencia de patrones concisa en las sentencias switch de C# para diferentes tipos.

Patrón Constante

Un patrón constante prueba si el resultado de una expresión es igual a un valor constante especificado. Esto simplifica las comparaciones de valores discretos.

Antes:

public string GetOrderStateOld(OrderStatus status)
{
    if (status == OrderStatus.Pending)
        return "Order is awaiting processing.";
    else if (status == OrderStatus.Shipped)
        return "Order has been dispatched.";
    else if (status == OrderStatus.Delivered)
        return "Order has been delivered.";
    else if (status == OrderStatus.Cancelled)
        return "Order has been cancelled.";
    else
        return "Unknown order state.";
}

public enum OrderStatus { Pending, Shipped, Delivered, Cancelled, Returned }

Después (usando el Patrón Constante):

public string GetOrderStateNew(OrderStatus status) => status switch
{
    OrderStatus.Pending => "Order is awaiting processing.",
    OrderStatus.Shipped => "Order has been dispatched.",
    OrderStatus.Delivered => "Order has been delivered.",
    OrderStatus.Cancelled => "Order has been cancelled.",
    _ => "Unknown order state."
};

Esta es una forma limpia de manejar la coincidencia de patrones de cadenas o valores de enumeración específicos en C#.

Patrones Relacionales

Los patrones relacionales comparan el resultado de una expresión con una constante utilizando operadores de comparación (<, >, <=, >=). Esto hace que las comprobaciones de rango sean mucho más legibles.

Antes:

public string GetEmployeePerformanceLevelOld(int score)
{
    if (score < 50)
        return "Needs Improvement";
    else if (score >= 50 && score < 70)
        return "Meets Expectations";
    else if (score >= 70 && score < 90)
        return "Exceeds Expectations";
    else if (score >= 90)
        return "Outstanding";
    else
        return "Invalid Score"; // Should not happen with int, but for completeness
}

Después (usando Patrones Relacionales):

public string GetEmployeePerformanceLevelNew(int score) => score switch
{
    < 50 => "Needs Improvement",
    >= 50 and < 70 => "Meets Expectations",
    >= 70 and < 90 => "Exceeds Expectations",
    >= 90 => "Outstanding",
    _ => "Invalid Score" // Catches any unexpected integer values
};

Esto muestra el poder de combinar patrones relacionales con patrones lógicos para la coincidencia de patrones en C#.

Patrón de Propiedad

El patrón de propiedad permite hacer coincidir las propiedades o campos de una expresión con patrones anidados. Esto es increíblemente útil para inspeccionar el estado de los objetos.

Antes:

public decimal CalculateOrderDiscountOld(CustomerOrder order)
{
    if (order == null) return 0m;
    if (order.TotalAmount >= 500 && order.Customer.IsPremium)
    {
        return 0.15m; // 15% for premium customers with large orders
    }
    else if (order.TotalAmount >= 100)
    {
        return 0.05m; // 5% for general large orders
    }
    return 0m;
}

public class Customer { public bool IsPremium { get; set; } }
public class CustomerOrder { public decimal TotalAmount { get; set; } public Customer Customer { get; set; } }

Después (usando el Patrón de Propiedad):

public decimal CalculateOrderDiscountNew(CustomerOrder order) => order switch
{
    { TotalAmount: >= 500, Customer.IsPremium: true } => 0.15m,
    { TotalAmount: >= 100 } => 0.05m,
    null => 0m,
    _ => 0m
};

Esto demuestra una potente coincidencia de patrones de propiedad en C#, simplificando las comprobaciones de estado de los objetos.

Patrón Posicional

Los patrones posicionales deconstruyen el resultado de una expresión (como tuplas o registros con métodos Deconstruct) y hacen coincidir los valores resultantes con patrones anidados.

Antes:

public string DescribeTransactionOld(Transaction transaction)
{
    if (transaction.Type == TransactionType.Deposit && transaction.Amount > 1000)
        return "Large Deposit";
    else if (transaction.Type == TransactionType.Withdrawal && transaction.Amount > 500)
        return "Significant Withdrawal";
    else if (transaction.Type == TransactionType.Fee && transaction.Amount > 0)
        return "Applied Fee";
    else
        return "Standard Transaction";
}

public record Transaction(TransactionType Type, decimal Amount);
public enum TransactionType { Deposit, Withdrawal, Fee, Transfer }

Después (usando el Patrón Posicional):

public string DescribeTransactionNew(Transaction transaction) => transaction switch
{
    (TransactionType.Deposit, > 1000m) => "Large Deposit",
    (TransactionType.Withdrawal, > 500m) => "Significant Withdrawal",
    (TransactionType.Fee, > 0m) => "Applied Fee",
    _ => "Standard Transaction"
};

Esta forma concisa mejora la legibilidad de la coincidencia de patrones en C# para tipos deconstruibles.

Patrones Lógicos (and, or, not)

Los patrones lógicos combinan otros patrones utilizando las palabras clave and, or y not, lo que permite comprobaciones condicionales complejas dentro de un solo patrón.

Antes:

public bool CanGrantAccessOld(UserRole role, int securityLevel, bool hasTwoFactorAuth)
{
    if ((role == UserRole.Administrator || role == UserRole.Manager) && securityLevel > 7 && hasTwoFactorAuth)
        return true;
    else if (role == UserRole.Developer && securityLevel > 5)
        return true;
    else if (role != UserRole.Guest && securityLevel >= 3 && securityLevel <= 10)
        return true;
    else
        return false;
}

public enum UserRole { Guest, User, Developer, Manager, Administrator }

Después (usando Patrones Lógicos):

public bool CanGrantAccessNew(UserRole role, int securityLevel, bool hasTwoFactorAuth) => (role, securityLevel, hasTwoFactorAuth) switch
{
    (UserRole.Administrator or UserRole.Manager, > 7, true) => true,
    (UserRole.Developer, > 5, _) => true,
    (not UserRole.Guest, >= 3 and <= 10, _) => true,
    _ => false
};

La coincidencia de patrones is en C# combinada con operadores lógicos se vuelve muy expresiva.

Patrón var

El patrón var siempre coincide con una expresión y asigna su resultado a una nueva variable local. Es particularmente útil cuando necesita capturar el valor de entrada completo de una expresión switch (o una parte de un patrón complejo) para realizar comprobaciones adicionales más complejas dentro de una cláusula when, especialmente aquellas que involucran llamadas a métodos o propiedades que no pueden expresarse directamente con otros patrones.

Antes:

using System.Collections.Generic;
using System.Linq;

public string AnalyzeNumberSequenceOld(IEnumerable<int> numbers)
{
    if (numbers == null)
        return "Invalid sequence (null)";
    if (numbers.Count() == 0)
        return "Empty sequence";
    if (numbers.Count() > 5 && numbers.Average() > 10.0)
        return "Long sequence with high average";
    return "Normal sequence";
}

Después (usando el Patrón var con la cláusula when):

using System.Collections.Generic;
using System.Linq;

public string AnalyzeNumberSequenceNew(IEnumerable<int> numbers) => numbers switch
{
    null => "Invalid sequence (null)", 
    var list when list.Count() == 0 => "Empty sequence",
    var list when list.Count() > 5 && list.Average() > 10.0 => "Long sequence with high average",
    _ => "Normal sequence"
};

Aquí, var list captura la instancia de IEnumerable<int>. Esto permite que las consultas LINQ complejas posteriores (como Count() y Average()) se realicen directamente sobre la variable list dentro de la cláusula when, sin necesidad de comprobaciones de tipo explícitas usando is.

Patrones de Lista (C# 11)

Introducidos en C# 11, los patrones de lista permiten la coincidencia con elementos en una secuencia (como arreglos o List<T>). Son increíblemente potentes para validar la estructura y el contenido de las colecciones.

Antes:

public bool ValidateLogEntryOld(string[] logParts)
{
    if (logParts == null || logParts.Length < 2) return false;

    if (logParts[0] == "ERROR" && logParts.Length >= 3 && logParts[1] == "AUTH" && logParts[2] == "FAILED")
        return true;
    if (logParts[0] == "INFO" && logParts.Length >= 2 && logParts[1] == "CONNECTED")
        return true;
    if (logParts[0] == "WARN" && logParts.Length >= 4 && logParts[1] == "DISK" && logParts[2] == "SPACE" && int.TryParse(logParts[3], out int space) && space < 10)
        return true;
    return false;
}

Después (usando Patrones de Lista):

public bool ValidateLogEntryNew(string[] logParts) => logParts switch
{
    ["ERROR", "AUTH", "FAILED", ..] => true, // Error authentication failed
    ["INFO", "CONNECTED", ..] => true,     // Client connected
    ["WARN", "DISK", "SPACE", var size, ..] when int.TryParse(size, out int space) && space < 10 => true, // Low disk space
    _ => false // Any other log entry
};

Esta es una característica potente para la coincidencia de patrones con secuencias, especialmente útil en escenarios como el análisis de argumentos de línea de comandos o entradas de registro con estructuras variables. El patrón .. (patrón de segmento) coincide con cero o más elementos, lo que proporciona flexibilidad.

Más Allá de lo Básico: Código Más Limpio con Coincidencia de Patrones

Los beneficios de la coincidencia de patrones se extienden más allá del mero “azúcar sintáctico”. Al adoptar estas características, los desarrolladores escriben inherentemente código que es:

  • Más Expresivo: Los patrones transmiten directamente la intención, haciendo que el código sea más fácil de leer y comprender de un vistazo. La lógica condicional compleja se vuelve intuitiva.
  • Más Conciso: La eliminación de bloques if-else repetitivos y conversiones de tipo explícitas reduce el volumen de código, lo que conlleva menos código que mantener y menos oportunidades de errores.
  • Más Seguro: Las expresiones switch, en particular, a menudo requieren una coincidencia de patrones exhaustiva. El compilador de C# ayuda advirtiendo a los desarrolladores si una expresión switch no cubre todos los valores de entrada posibles, lo que ayuda a prevenir excepciones en tiempo de ejecución. Esta exhaustividad impuesta por el compilador mejora la corrección del software.
  • Más Fácil de Refactorizar: Con una lógica condicional más limpia y modular, la refactorización se vuelve menos abrumadora. Los desarrolladores pueden modificar o extender el comportamiento con un impacto mínimo en otras partes del sistema.

La coincidencia de patrones ha transformado C# en un lenguaje que ofrece soluciones elegantes para el manejo de datos y el flujo de control complejos. Desde comprobaciones básicas de tipo hasta la deconstrucción de objetos y la validación de secuencias, las capacidades proporcionadas por C# 8.0+ son herramientas esenciales para cualquier desarrollador de software moderno. Adoptar estas potentes características no solo mejora la calidad del código, sino que también agiliza el proceso de desarrollo, contribuyendo a aplicaciones más fiables y fáciles de mantener. Los desarrolladores que aún dependen de estilos más antiguos de sentencias if-else o switch pueden beneficiarse enormemente al incorporar estos patrones, haciendo que sus programas C# sean más robustos y agradables de trabajar.

Artículos relacionados