22 mayo 2025
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.
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.
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.
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.
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.
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#.
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#.
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.
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.
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.
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
.
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.
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:
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.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.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.