Novedades de C# 8: Pattern Matching

Tiempo de lectura: 6 minutos
Imagen ornamental para la entrada de C#8 pattern matching

En la última entrada hablamos del nuevo IAsyncEnumerable, y hoy es el turno de ver entra novedad que nos trae c#8, el ‘pattern matching’. Esta es una característica muy interesante y muy útil que nos puede ahorrar complejidad a la hora de escribir el código, y un buen número de líneas dentro de él.

Por si aún no conoces cómo funciona el pattern matching en C# o no te suena de que estoy hablando, debes saber que es una característica que se introdujo en C# 7 y que nos permitía reconocer objetos tipados dentro de otro tipo y castear directamente al tipo si se cumple. Podríamos decir que es una especia de ‘is’ seguido de un ‘as’:

//C# 6
if (dato is string)
{
    var valor = dato as string;
    Console.WriteLine("Dato es un string y su valor es: " + valor);
}

//C#7
if (dato is string valor)
{
    Console.WriteLine("Dato es un string y su valor es: " + valor);
}

Pattern matching en C# 8

Con la llegada de C# 8, esto ha mejorado aun más, y ha añadido algunas opciones bastante interesantes:

  • Expresiones switch
  • Switch con propiedades
  • Switch con tuplas
  • Switch posicional

Vamos a verlos en profundidad:

Expresiones switch

Con los cambios de C# 7 podemos utilizar un pattern matching como el que hemos visto arriba en un ‘switch’, pero gracias a esta nueva característica de C# 8, vamos a poder crear expresiones para los switch. Las expresiones reemplazan el ‘case :’ y el ‘break’ por ‘=>’ y el ‘defaul :’ por ‘_’, además, el objeto sobre el que vamos a hacer el switch va delante de este (y no detrás como hasta ahora) para indicar que se trata de una expresión. Vamos a ver un ejemplo:

//Version en C# 7
public static char GetLetraInstruccion(int code)
{
    switch (code)
    {
        case 1:
            return 'a';
        case 2:
            return 'b';
        default:
            return '_';
    }
}
//Version en C# 8
public static char GetLetraExpresion(int code)
{
    return code switch
    {
        1 => 'a',
        2 => 'b',    
        _ => '_' // Default
    };
}

//Versión en C# 8 con arrow functions
public static char GetLetraExpresion2(int code) => code switch
{
    1 => 'a',
    2 => 'b',
    _ => '_' //Default
};

Como puedes comprobar, esta versión de C# y su pattern matching junto a las arrow fuctions nos deja un código mucho más limpio.

Switch con propiedades

Utilizando el pattern matching de C# 7, podíamos utilizar las propiedades del objeto para aplicar condiciones extra en los switch:

public class Direccion
{
    public string Pais { get; set; }
    public string Comunidad { get; set; }
}
//...
[Flags]
public enum Idiomas
{
    Castellano = 1,
    Gallego = 2,
    Euskera = 4,
    Catalan = 8,
}
//...
//Version en C# 7
public static Idiomas GetIdiomasC7(Direccion direccion)
{
    switch (direccion)
    {
        case Direccion dir when dir.Pais == "ES" && dir.Comunidad == "Galicia":
            return Idiomas.Castellano | Idiomas.Gallego;
        case Direccion dir when dir.Pais == "ES" && dir.Comunidad == "Pais Vasco":
            return Idiomas.Castellano | Idiomas.Euskera;
        case Direccion dir when dir.Pais == "ES" && dir.Comunidad == "Cataluña":
            return Idiomas.Castellano | Idiomas.Catalan;
        case Direccion dir when dir.Pais == "ES":
            return Idiomas.Castellano;
        default:
            return Idiomas.Ingles;
    }
}

Esto ha cambiado gracias a C# 8 y su pattern matching, gracias al reconocimiento de propiedades y a las expresiones switch, vamos a poder dejar un código más limpio accediendo directamente a sus propiedades:

public static Idiomas GetIdiomasC8(Direccion direccion)
{
    return direccion switch
    {
        { Pais: "ES", Comunidad: "Galicia" } => Idiomas.Castellano | Idiomas.Gallego,
        { Pais: "ES", Comunidad: "Pais Vasco" } => Idiomas.Castellano | Idiomas.Euskera,
        { Pais: "ES", Comunidad: "Cataluña" } => Idiomas.Castellano | Idiomas.Catalan,
        { Pais: "ES" } => Idiomas.Castellano,
        _ => Idiomas.Ingles //Default
    };
}

Con esto que hemos visto, el código queda mucho más limpio y claro ¿verdad? Hasta este momento solo hemos visto cambios que son azúcar sintáctico, son útiles, son bonitos, pero no aportan nada nuevo respecto a lo que ya teníamos disponible en la versión anterior del C#. Entonces, ¿qué trae nuevo el pattern matching de C# 8? ¿Hay algo que realmente sea nuevo? ¡Sí!, vamos con ello.

Switch con tuplas

Hasta ahora, un switch era un flujo de control que se operaba solo sobre una variable. Si necesitábamos conjugar dos o más variables solo nos quedaba utilizar un if, pero if else tiene un problema de rendimiento cuando aumenta mucho de tamaño ya que para llegar al último tiene que ir comprobando todos los anteriores, cosa que con un switch no pasa.

Como dato, aproximadamente a partir de 5 posibles caminos dentro de un if else, ya es más ‘barato’ en términos de CPU el utilizar un switch que siempre va a tardar lo mismo independientemente del número de posibles caminos y cual sea el elegido. Da igual que sea el primero o el último.

Precisamente esta nueva característica una de las dos que se han introducido en C# 8 y su pattern matching, permitiéndonos utilizar una tupla como objeto para el switch, y una tupla como condición, fíjate que limpito queda ahora un «Piedra papel o tijera, lagarto Spock«:

public static string PiedraPapelTijeraLagartoSpock(string playerA, string playerB)
{
    return (playerA, playerB) switch
    {
        ("piedra", "papel") => "Gana papel",
        ("piedra", "tijera") => "Gana piedra",
        ("piedra", "lagarto") => "Gana piedra",
        ("piedra", "spock") => "Gana Spock",
        ("papel", "piedra") => "Gana papel",
        ("papel", "tijera") => "Gana tijera",
        ("papel", "lagarto") => "Gana lagarto",
        ("papel", "spock") => "Gana papel",
        ("tijera", "piedra") => "Gana piedra.",
        ("tijera", "papel") => "Gana tijera",
        ("tijera", "lagarto") => "Gana tijera.",
        ("tijera", "spock") => "Gana Spock",
        ("lagarto", "piedra") => "Gana piedra",
        ("lagarto", "papel") => "Gana lagarto",
        ("lagarto", "tijera") => "Gana tijero",
        ("lagarto", "spock") => "Gana lagarto",
        ("spock", "piedra") => "Gana Spock.",
        ("spock", "papel") => "Gana papel",
        ("spock", "tijera") => "Gana Spock.",
        ("spock", "lagarto") => "Gana lagarto",
        (_, _) => "empate" //Default
    };
}

En el caso anterior, como en todos los que se usen expresiones switch, ‘_’ significa cualquier valor, por lo que puedes tener errores porque exista algún código que es inalcanzable si las pones en el orden erróneo (por ejemplo {_,_} el primero de todos).

Vale, ya hemos visto una utilidad que realmente es nueva, pero… ¿Quedan más? Por supuesto, vamos a por la última.

Switch posicional

Una característica de C# 7 era la deconstrucción de clases. Esta característica nos va a permitir si tenemos un método Deconstruct en la clase que sea accesible, obtener una tupla con los valores que generemos con la deconstrucción. Por ejemplo:

public class Posicion
{
    private readonly double _x;
    private readonly double _y;

    public Posicion(double x, double y)
    {
        _x = x;
        _y = y;
    }

    public void Deconstruct(out double x, out double y)
    {
        x= _x;
        y= _y;
    }
}

//
var posicion = new Posicion(10,20);
var (x, y) = posicion;

En el caso anterior veíamos que podemos trabajar con tuplas perfectamente, y esto es una tupla… ¿Dónde está lo nuevo?

Precisamente el caso anterior trabajábamos con diferentes casuísticas de tuplas, pero ahora vamos a poder trabajar con los valores de la tupla para hacer la condición de nuestro switch, por ejemplo:

public enum Cuadrantes
{
    SuperiorDerecho,
    SuperiorIzquierdo,
    InferiorIzquierdo,
    InferiorDerecho,
    Origen
}

public static Cuadrantes GetCuadrante(Posicion posicion)
{
    return posicion switch
    {
        var (x, y) when x > 0 && y > 0 => Cuadrantes.SuperiorDerecho,
        var (x, y) when x < 0 && y > 0 => Cuadrantes.SuperiorIzquierdo,
        var (x, y) when x < 0 && y < 0 => Cuadrantes.InferiorIzquierdo,
        var (x, y) when x > 0 && y < 0 => Cuadrantes.InferiorDerecho,
        _ => Cuadrantes.Origen
    };
}

Si te detienes en el código anterior, puedes comprobar que estamos haciendo un switch deconstruyendo el objeto, y después filtrándolo por sus diferentes propiedades deconstruidas.

En realidad, esta nueva característica del pattern matching de C# 8 es igual que al anterior ya que ambas trabajan con tuplas (de hecho, en el caso anterior también podemos utilizar variables y usar when como en ese), pero aportándonos algo de azúcar sintáctico para ahorrarnos tener que añadir un paso extra para obtener la tupla.

Puede que a simple vista parezca algo raro y poco útil, es la reacción habitual a los cambios. Una vez que te acostumbres a utilizar las expresiones switch y el resto de las características, no vas a querer dejar de usarlas ya que deja un código mucho más limpio y claro.

Si tienes cualquier duda o comentario, no te lo pienses y adelante!!

En las próximas semanas hablaremos sobre indices y rangos, y sobre el las nuevas interfaces de C#8.

Deja un comentario