Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

18 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 21 de marzo de 2023
C#

El pattern matching de C# proporciona la capacidad de analizar expresiones para ver si cumplen determinados "patrones" o presentan características determinadas. Podéis ver algunos ejemplos interesantes en el post Un vistazo a los patrones relacionales y combinadores.

Aunque ya los tengo bastante interiorizados y hago uso de ellos cuando toca, todavía no se me ha dado el caso de necesitar los patrones de listas, introducidos hace unos meses en C# 11. Así que no está de más echarles un vistazo para cuando se dé la ocasión 😉

Qué son los list patterns o patrones de listas

Esta funcionalidad de C# nos permite comprobar si los elementos de una secuencia siguen el patrón que indiquemos, con la posibilidad adicional de poder extraer valores o rangos de valores cuando nos interese.

Esto se entiende mucho mejor con ejemplos, así que vamos a ver algunos. Probablemente el escenario más sencillo que podemos encontrar es algo como lo siguiente:

int[] numbers = { 1, 2, 3, 4, 5};

Console.WriteLine(numbers is [1, 2, 3, 4, 5]); // true
Console.WriteLine(numbers is [1, 2, 3]);       // false

Seguro que podéis entender el código a simple vista. Usamos el operador is sobre el array [1, 2, 3, 4, 5] para preguntar si sus valores coinciden exactamente con los patrones que andamos buscando. En el primer caso el matching es completo, pero no ocurre lo mismo con el segundo patrón, donde buscamos un array de tres elementos con valores [1, 2, 3].

Ojo, es importante tener en cuenta que los valores que usamos en el patrón deben ser constantes. Por tanto, el siguiente código fallaría en compilación:

int one = 1, two = 2;
int[] numbers = { one, two, 3, 4, 5};
Console.WriteLine(numbers is [one, two, 3, 4, 5]); // Constant value expected (CS0150)

Por esta razón, podemos usar este tipo de pattern matching únicamente con tipos de datos que podamos representar en el código como una constante, como char, string, bool, o todos los numéricos:

string[] letters = { "a", "b", "c" };
Console.WriteLine(letters is ["a", "b", "c"]); // true

También es importante saber que podemos usar list patterns con cualquier tipo de colección, siempre que sea contable (es decir, que tenga una propiedad Length o Count) y troceable (que permita la obtención de slices o porciones basadas en rangos). Por tanto, el siguiente código será válido:

var numberList = new List<int>(){ 1, 2, 3 };
Console.WriteLine(numberList is [1, 2, 3]); 

Pues seguro que estás pensando que lo que hemos visto hasta ahora no es demasiado útil, pero claro, solo hemos arañado muy ligeramente la superficie de los list patterns. Vamos a seguir viendo otros escenarios mucho más potentes, y a los que probablemente veréis más utilidad.

El patrón slice ".."

Los ejemplos anteriores buscaban el matching en el conjunto completo de elementos, pero tenemos forma de comprobar exclusivamente inicios o finales de la secuencia usando el patrón slice "..".

Por ejemplo, fijaos qué forma más curiosa e interesante de comprobar los primeros y últimos elementos de la lista:

int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

Console.WriteLine(numbers is [1, .., 9]);       // True
Console.WriteLine(numbers is [1, 2, .., 8, 9]); // True

Básicamente, usamos la sintaxis .. para indicar que nos da igual lo que vaya ahí dentro, siempre que se cumpla la parte constante del patrón que buscamos al principio y al final.

Esto podríamos usarlo también usarlo para buscar exclusivamente inicios o finales:

int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

Console.WriteLine(numbers is [.., 8, 9]); // True
Console.WriteLine(numbers is [1, 2, ..]); // True

Console.WriteLine(numbers is [.., 8]);    // False
Console.WriteLine(numbers is [2 ,..]);    // False

Hay tres cosillas interesantes que añadir en este punto.

Primero, los dos puntos del slicing solo se pueden introducir una vez en cada patrón. Es decir, el siguiente código fallaría en compilación:

int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

// Error CS8980: Slice patterns may only be used once
//               and directly inside a list pattern.
Console.WriteLine(numbers is [1, .., 5, .., 9]);

Segundo, y muy importante, la porción comprobada por el patrón slice podría estar vacía y aún así seguiría cumpliendo el patrón. Es decir, cuando usemos ".." para comprobar elementos de una secuencia, el matching se producirá aunque no haya nada dentro, como podemos ver a continuación:

int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers is [1, 2, 3]);      // True
Console.WriteLine(numbers is [1, .., 3]);     // True
Console.WriteLine(numbers is [1, 2, .., 3]);  // True
Console.WriteLine(numbers is [1, 2, 3, ..]);  // True
Console.WriteLine(numbers is [.., 1, 2, 3]);  // True

Tercero, el patrón slicing permite el uso de subpatrones para restringir los valores que pasarán el filtro. En el siguiente ejemplo podemos ver cómo utilizar esta característica para detectar secuencias que comiencen con 1, acaben con 5, y entre medios tengan entre 2 y 5 elementos:

int[] numbers = { 1, 2, 3, 4, 5 };
Console.WriteLine(numbers is [1, .. { Length : >=2 and <=5}, 5]);     // True

int[] moreNumbers = { 1, 2, 5 };
Console.WriteLine(moreNumbers is [1, .. { Length : >=2 and <=5}, 5]); // False

El patrón de descarte "_"

El carácter de descarte _ podemos usarlo como placeholder para representar un único elemento, cuyo valor no nos interesa.

Por ejemplo, a continuación vemos que el matching se produce cuando una secuencia de cinco elementos exactos comienza con 1 y 2, y finaliza con 5:

int[] numbers = { 1, 2, 3, 4, 5 };
Console.WriteLine(numbers is [1, 2, _, _, 5]); 

Obviamente, el patrón de descarte "_" puede combinarse con el de slicing. En los siguientes ejemplo, buscamos una lista que comience por cualquier valor seguido del número 2, acabando en 5:

int[] numbers = { 1, 2, 3, 4, 5 };
Console.WriteLine(numbers is [_, 2, .., 5]); // True

Patrones relacionales y combinadores

Como vimos en un post anterior, los patrones relacionales permiten usar el conjunto de operadores relacionales <, <=, >, y >= para detectar los valores deseados, en lugar de usar simples constantes.

Pues bien, esta capacidad está también disponible en en los patrones de lista, como podemos ver a continuación. En el primer ejemplo, buscamos una secuencia de cualquier número de elementos que comience y acabe por un número mayor que cero:

int[] numbers = new[] { 1, 2, 3, 4, 5 };
Console.WriteLine(numbers is [> 0, .., > 0]); // True

Los operadores relacionales podemos usarlo en el lugar que introduciríamos un valor constante. El siguiente ejemplo complica algo más el patrón, con relacionales especificando valores posibles en cada entrada de la secuencia:

int[] numbers = new[] { 1, 2, 3, 4, 5 };
Console.WriteLine(numbers is [> 0, <= 2, <= 3, < 5, > 4]); // True

También en el post al que hacíamos referencia vimos que los combinadores permitían usar las palabras clave and y or para evaluar condiciones compuestas, lo que permite expresar patrones aún más complejos.

Por ejemplo, a continuación vemos el patrón que encajaría con una lista que comience por un valor entre 1 y 5, y acabe con el valor 5 u 8:

int[] numbers = new[] { 1, 2, 3, 4, 5 };
Console.WriteLine(numbers is [>= 1 and <= 5, .., 5 or 8]); // True

Extracción de valores: el patrón var

Cuando el matching se produce, podemos aprovechar la ocasión para extraer valores de la secuencia.

int[] numbers = new[] { 1, 2, 3, 4, 5 };
if (numbers is [var first, .., var last])
{
    Console.WriteLine($"{first}, {last}");
}

Pero ojo, porque los valores de first y last solo serán establecidos cuando cuando se detecte la coincidencia entre la secuencia de entrada y el patrón especificado. Por tanto, el ámbito de las variables estará restringido al bloque de código de la condición donde se realice la comprobación de matching, lo que provocará que el siguiente código no compile:

if (numbers is [var first, .., var last])
{
    // first y last son visibles aquí:
    Console.WriteLine($"{first}, {last}");
}
// Pero no aquí:
Console.WriteLine(first); // CS0165: Use of unassigned local variable 'first'

También podemos usar el patrón var para capturar el contenido de rangos completos. En el siguiente ejemplo buscamos secuencias que comiencen y acaben con unos valores determinados, y extraemos al mismo tiempo elementos centrales:

int[] numbers = { 1, 2, 3, 4, 5 };

if (numbers is [1, .. var elems, 5])
{
    Console.WriteLine(string.Join(", ", elems)); // 2, 3, 4
}

Bueno, pues creo que hemos visto lo más interesante de los patrones de lista, así que deberíamos tener suficiente para aplicarlos cuando sea necesario. Espero que os haya resultado interesante :)

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!