Información general sobre la coincidencia de patrones

La coincidencia de patrones es una técnica consistente en probar una expresión para determinar si tiene ciertas características. La coincidencia de patrones de C# proporciona una sintaxis más concisa para probar las expresiones y realizar acciones cuando una expresión coincide. La "expresión is" admite la coincidencia de patrones para probar una expresión y declarar condicionalmente una nueva variable como el resultado de esa expresión. La expresión switch permite realizar acciones basadas en el primer patrón que coincida para una expresión. Estas dos expresiones admiten un variado vocabulario de patrones.

En este artículo encontrará información general sobre los escenarios en los que puede usar la coincidencia de patrones. Estas técnicas pueden mejorar la legibilidad y la corrección del código. Para ver una explicación detallada de todos los patrones que puede aplicar, consulte el artículo sobre patrones en la referencia del lenguaje.

Comprobaciones de valores null

Uno de los escenarios más comunes para usar la coincidencia de patrones es asegurarse de que los valores no son null. Puede probar y convertir un tipo de valor que admite valores null a su tipo subyacente mientras prueba null con el ejemplo siguiente:

int? maybe = 12;

if (maybe is int number)
{
    Console.WriteLine($"The nullable int 'maybe' has the value {number}");
}
else
{
    Console.WriteLine("The nullable int 'maybe' doesn't hold a value");
}

El código anterior es un patrón de declaración para probar el tipo de la variable y asignarlo a una nueva variable. Las reglas de lenguaje hacen que esta técnica sea más segura que muchas otras. Solo es posible acceder a la variable number y asignarla en la parte verdadera de la cláusula if. Si intenta acceder a ella en otro lugar, ya sea en la cláusula else o después del bloque if, el compilador emitirá un error. En segundo lugar, como no usa el operador ==, este patrón funciona cuando un tipo sobrecarga el operador ==. Esto lo convierte en una manera ideal de comprobar los valores de referencia nula, incorporando el patrón not:

string? message = ReadMessageOrDefault();

if (message is not null)
{
    Console.WriteLine(message);
}

En el ejemplo anterior se ha usado un patrón constante para comparar la variable con null. not es un patrón lógico que coincide cuando el patrón negado no coincide.

Pruebas de tipo

Otro uso común de la coincidencia de patrones consiste en probar una variable para ver si coincide con un tipo determinado. Por ejemplo, el código siguiente comprueba si una variable no es null e implementa la interfaz System.Collections.Generic.IList<T>. Si es así, usa la propiedad ICollection<T>.Count de esa lista para buscar el índice central. El patrón de declaración no coincide con un valor null, independientemente del tipo de tiempo de compilación de la variable. El código siguiente protege contra null, además de proteger contra un tipo que no implementa IList.

public static T MidPoint<T>(IEnumerable<T> sequence)
{
    if (sequence is IList<T> list)
    {
        return list[list.Count / 2];
    }
    else if (sequence is null)
    {
        throw new ArgumentNullException(nameof(sequence), "Sequence can't be null.");
    }
    else
    {
        int halfLength = sequence.Count() / 2 - 1;
        if (halfLength < 0) halfLength = 0;
        return sequence.Skip(halfLength).First();
    }
}

Se pueden aplicar las mismas pruebas en una expresión switch para probar una variable con varios tipos diferentes. Puede usar esa información para crear algoritmos mejores basados en el tipo de tiempo de ejecución específico.

Comparación de valores discretos

También puede probar una variable para buscar una coincidencia con valores específicos. En el código siguiente se muestra un ejemplo en el que se prueba un valor con todos los valores posibles declarados en una enumeración:

public State PerformOperation(Operation command) =>
   command switch
   {
       Operation.SystemTest => RunDiagnostics(),
       Operation.Start => StartSystem(),
       Operation.Stop => StopSystem(),
       Operation.Reset => ResetToReady(),
       _ => throw new ArgumentException("Invalid enum value for command", nameof(command)),
   };

En el ejemplo anterior se muestra un envío de método basado en el valor de una enumeración. El caso final _ es un patrón de descarte que coincide con todos los valores. Controla todas las condiciones de error en las que el valor no coincide con uno de los valores enum definidos. Si omite ese segmento modificador, el compilador le advierte de que la expresión de patrón no controla todos los valores de entrada posibles. En tiempo de ejecución, la expresión switch produce una excepción si el objeto que se está examinando no coincide con ninguno de los segmentos modificadores. Puede usar constantes numéricas en lugar de un conjunto de valores de enumeración. También puede usar esta técnica similar para los valores de cadena constantes que representan los comandos:

public State PerformOperation(string command) =>
   command switch
   {
       "SystemTest" => RunDiagnostics(),
       "Start" => StartSystem(),
       "Stop" => StopSystem(),
       "Reset" => ResetToReady(),
       _ => throw new ArgumentException("Invalid string value for command", nameof(command)),
   };

En el ejemplo anterior se muestra el mismo algoritmo, pero se usan valores de cadena en lugar de una enumeración. Usaría este escenario si la aplicación responde a comandos de texto, en lugar de a un formato de datos normal. A partir de C# 11, también puede usar Span<char> o ReadOnlySpan<char> para probar los valores de cadena constantes, tal como se muestra en el ejemplo siguiente:

public State PerformOperation(ReadOnlySpan<char> command) =>
   command switch
   {
       "SystemTest" => RunDiagnostics(),
       "Start" => StartSystem(),
       "Stop" => StopSystem(),
       "Reset" => ResetToReady(),
       _ => throw new ArgumentException("Invalid string value for command", nameof(command)),
   };

En todos estos ejemplos, el patrón de descarte le garantiza que controlará todas las entradas. El compilador le ayuda a asegurarse de que se controlan todos los valores de entrada posibles.

Patrones relacionales

Puede usar patrones relacionales para probar cómo se compara un valor con las constantes. Por ejemplo, el código siguiente devuelve el estado del agua en función de la temperatura en Fahrenheit:

string WaterState(int tempInFahrenheit) =>
    tempInFahrenheit switch
    {
        (> 32) and (< 212) => "liquid",
        < 32 => "solid",
        > 212 => "gas",
        32 => "solid/liquid transition",
        212 => "liquid / gas transition",
    };

El código anterior también muestra el andpatrón lógico conjuntivo para comprobar que ambos patrones relacionales coincidan. También puede usar un patrón or disyuntivo para comprobar que cualquiera de los patrones coincide. Los dos patrones relacionales están entre paréntesis, que se pueden usar alrededor de cualquier patrón para mayor claridad. Los dos últimos segmentos modificadores controlan los casos del punto de fusión y el punto de ebullición. Sin esos dos segmentos, el compilador le advertirá de que la lógica no abarca todas las entradas posibles.

El código anterior también muestra otra característica importante que el compilador proporciona para las expresiones de coincidencia de patrones: el compilador le advierte si no controla todos los valores de entrada. El compilador emite además una advertencia si el patrón de una parte de la expresión switch está controlado por un patrón anterior. Esto le da libertad para refactorizar y reordenar expresiones switch. La misma expresión también se podría escribir así:

string WaterState2(int tempInFahrenheit) =>
    tempInFahrenheit switch
    {
        < 32 => "solid",
        32 => "solid/liquid transition",
        < 212 => "liquid",
        212 => "liquid / gas transition",
        _ => "gas",
};

La lección más importante del ejemplo anterior, y cualquier otra refactorización o reordenación, es que el compilador valida que el código controla todas las entradas posibles.

Varias entradas

Todos los patrones cubiertos hasta ahora comprueban una entrada. Puede escribir patrones que examinen varias propiedades de un objeto. Fíjese en el siguiente registro Order:

public record Order(int Items, decimal Cost);

El tipo de registro posicional anterior declara dos miembros en posiciones explícitas. Primero aparece Items y, luego, el valor Cost del pedido. Para obtener más información, consulte Registros.

El código siguiente examina el número de elementos y el valor de un pedido para calcular un precio con descuento:

public decimal CalculateDiscount(Order order) =>
    order switch
    {
        { Items: > 10, Cost: > 1000.00m } => 0.10m,
        { Items: > 5, Cost: > 500.00m } => 0.05m,
        { Cost: > 250.00m } => 0.02m,
        null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
        var someObject => 0m,
    };

Los dos primeros segmentos examinan dos propiedades de Order. El tercero examina solo el costo. El siguiente realiza la comprobación de null y el último coincide con cualquier otro valor. Si el tipo Order define un método Deconstruct adecuado, puede omitir los nombres de propiedad del patrón y usar la desconstrucción para examinar las propiedades:

public decimal CalculateDiscount(Order order) =>
    order switch
    {
        ( > 10,  > 1000.00m) => 0.10m,
        ( > 5, > 50.00m) => 0.05m,
        { Cost: > 250.00m } => 0.02m,
        null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
        var someObject => 0m,
    };

El código anterior muestra el patrón posicional donde las propiedades se deconstruyen para la expresión.

Patrones de lista

Puede comprobar los elementos de una lista o una matriz mediante un patrón de lista. Un patrón de lista proporciona una forma de aplicar un patrón a cualquier elemento de una secuencia. Además, puede aplicar el patrón de descarte (_) para que coincida con cualquier elemento, o bien un patrón de sector para que coincida con ninguno o más elementos.

Los patrones de lista son una herramienta valiosa cuando los datos no siguen una estructura normal. Puede usar la coincidencia de patrones para probar la forma y los valores de los datos en vez de transformarlos en un conjunto de objetos.

Tenga en cuenta el siguiente extracto de un archivo de texto que contiene transacciones bancarias:

04-01-2020, DEPOSIT,    Initial deposit,            2250.00
04-15-2020, DEPOSIT,    Refund,                      125.65
04-18-2020, DEPOSIT,    Paycheck,                    825.65
04-22-2020, WITHDRAWAL, Debit,           Groceries,  255.73
05-01-2020, WITHDRAWAL, #1102,           Rent, apt, 2100.00
05-02-2020, INTEREST,                                  0.65
05-07-2020, WITHDRAWAL, Debit,           Movies,      12.57
04-15-2020, FEE,                                       5.55

Es un formato CSV, pero algunas de las filas tienen más columnas que otras. También hay algo incluso peor para el procesamiento, una columna del tipo WITHDRAWAL que contiene texto generado por el usuario y puede contener una coma en el texto. Un patrón de lista que incluye el patrón de descarte, el patrón constante y el patrón var para capturar los datos de los procesos del valor en este formato:

decimal balance = 0m;
foreach (string[] transaction in ReadRecords())
{
    balance += transaction switch
    {
        [_, "DEPOSIT", _, var amount]     => decimal.Parse(amount),
        [_, "WITHDRAWAL", .., var amount] => -decimal.Parse(amount),
        [_, "INTEREST", var amount]       => decimal.Parse(amount),
        [_, "FEE", var fee]               => -decimal.Parse(fee),
        _                                 => throw new InvalidOperationException($"Record {string.Join(", ", transaction)} is not in the expected format!"),
    };
    Console.WriteLine($"Record: {string.Join(", ", transaction)}, New balance: {balance:C}");
}

En el ejemplo anterior se toma una matriz de cadenas, donde cada elemento es un campo de la fila. Las claves de expresión switch del segundo campo, que determinan el tipo de transacción y el número de columnas restantes. Cada fila garantiza que los datos están en el formato correcto. El patrón de descarte (_) omite el primer campo, con la fecha de la transacción. El segundo campo coincide con el tipo de transacción. Las coincidencias restantes del elemento saltan hasta el campo con la cantidad. La coincidencia final usa el patrón var para capturar la representación de cadena de la cantidad. La expresión calcula la cantidad que se va a agregar o restar del saldo.

Los patrones de lista permiten buscar coincidencias en la forma de una secuencia de elementos de datos. Los patrones de descarte y de sector se usan para hallar coincidencias con la ubicación de los elementos. Se usan otros patrones para que coincidan con las características de los elementos individuales.

En este artículo se proporciona una introducción a los tipos de código que se pueden escribir con la coincidencia de patrones en C#. En los artículos siguientes se muestran más ejemplos del uso de patrones en escenarios y un vocabulario completo de patrones disponibles.

Vea también