martes, 1 de junio de 2021

Un vistazo a los patrones relacionales y combinadores, lo nuevo de C# 9 para exprimir el pattern matching

.NET Core

Desde C# 7 podemos emplear patrones en expresiones is o bloques switch para especificar las condiciones que deseamos comprobar, y cada nueva versión del lenguaje sigue introduciendo novedades al pattern matching, haciéndolo cada vez más sencillo y cómodo de utilizar.

En particular, C# 9 ha añadido un montón de características interesantes destinadas a facilitar el uso de patrones, pero en este post vamos a centrarnos en las dos más importantes: los combinadores de patrones y los patrones relacionales.

Los patrones relacionales permiten especificar restricciones utilizando los operadores relacionales <, <=, >, y >= para comparar el valor de la expresión a comprobar con constantes de los tipos soportados (sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, nint, y nuint).

Podemos ver un ejemplo sencillo de su uso en el siguiente bloque switch, donde, en lugar de introducir valores constantes en los casos, usamos los operadores relacionales para especificar las condiciones a cumplir en cada uno de ellos:

string result;
switch(i)
{
    case 1:
        result = "One";
        break;
    case < 3: 
        result = "Less than 3";
        break;
    case < 5:
        result = "Less than 5";
        break;
    case >= 5:
        result = "Equal or greater than 5";
        break;
};

Ciertamente, en este código la forma de expresar las condiciones es concisa y fácilmente legible, aunque el bloque switch tradicional es demasiado verboso. Por ello, sería mejor replantear el código anterior con una expresión switch, lo que reducirá significativamente la cantidad de código a emplear (de 15 a 6 líneas), manteniendo la funcionalidad intacta:

var result = i switch
{
    1    =>  "One",
    < 3  =>  "Less than 3",
    < 5  =>  "Less than 5",
    >= 5 => "Equal or greater than 5",
};

Por otra parte, los combinadores de patrones introducen las nuevas las palabras clave and y or, que permiten evaluar condiciones compuestas por más de un patrón, y not, cuyo objetivo es negar la evaluación de un patrón. Combinando estos nuevos operadores podemos expresar de forma sencilla y fácilmente legible restricciones mucho más complejas que en versiones anteriores del lenguaje.

Por ejemplo, observad lo claro que resulta el siguiente bloque de código, imposible de crear antes de C#9:

var result = i switch
{
    10 or 11       => "10-11",
    >= 0 and <= 3  => "0-3",
    <= 4 and <= 6  => "4-6",
    >= 7           => "7-",
};

El compilador analizará las restricciones y será capaz de arrojarnos un error cuando existan ambigüedades o errores, como en el siguiente ejemplo:

var result = i switch
{
    >= 0 and <= 3 => "0-3",
    >= 2 and <= 3 => "2-3", // No compila, está incluido en el anterior
    _ => "Otros"
};

Este mismo tipo de condiciones pueden utilizarse en bloques if a través del operador is:

if (i is 9 or 8) { ... }
if (i is >= 3 and <= 8) { ... }

En cuanto al combinador not, probablemente, uno de los ejemplos más claros de su uso lo vimos ya hace unos días, utilizándolo para determinar cuándo un objeto no es nulo:

if(invoice is not null)  { ... }

También podríamos usarlo en construcciones más complejas, donde ganamos bastante en legibilidad si lo comparamos con las versiones anteriores de C#. Veamos varios ejemplos.

En primer lugar, imaginad que necesitamos comprobar que un objeto no sea nulo, ni de un tipo determinado. Así es como se podría implementar en las dos últimas versiones del lenguaje:

// C# 8:
if(!(obj is null) && !(obj is Invoice)) 
{ 
    // Si obj no es nulo, pero tampoco es un Invoice
}

// C# 9:
if (obj is not null and not Invoice)
{
    // Sin comentarios, ¡el código se entiende solo!
}

Veamos ahora cómo determinar si un entero es distinto a un conjunto de valores determinados (en este caso, los impares en el rango 0-10):

// C# 8:
if(num != 1 &&  num != 3 && num != 5 && num != 7 && num != 9) 
{
    // Num no es 1, 3, 5, 7, ni 9
}

// C# 9:
if (num is not (1 or 3 or 5 or 7 or 9))
{
    // Num no es 1, 3, 5, 7, ni 9
}

Fijamos que en este caso hemos utilizado paréntesis para introducir prioridades en la expresión. Es algo, también introducido en C# 9, que potencia aún más las capacidades de este tipo de construcciones.

El mismo nivel de concisión y elegancia podríamos lograrlo también al operar con enums, lo cual nos resolvería un escenario bastante frecuente:

// C# 9:
if (result.StatusCode is not (HttpStatusCode.OK or HttpStatusCode.Created))
{
    // También con enums
}

¿Interesante, verdad? En definitiva, creo que estas incorporaciones al lenguaje son bastante potentes y probablemente acabarán popularizando definitivamente el uso de patrones en C#, que hasta ahora era bastante limitado.

Como ocurre siempre que hay cambios de este tipo, tendremos que acostumbrarnos al uso de una nueva sintaxis, pero estoy seguro de que el pequeño esfuerzo valdrá la pena y al final saldremos ganando porque en muchas ocasiones podremos expresar lo mismo en menos espacio, y ganando en claridad :)

Publicado en Variable not found.

No hay comentarios:

Publicar un comentario