Menú de navegaciónMenú
Categorías

La mejor forma de Aprender Programación online y en español www.campusmvp.es

?id=44850497-ea4a-455f-8ed5-ffc4d2589f49

Lenguaje C#: coincidencia de modelos - Parte 2: Uso de expresiones en el switch

Imagen ornamental

En la entrega anterior de este artículo hemos visto cómo podemos sacar partido a la coincidencia de patrones tanto en condicionales como en expresiones switch para simplificar el código, dándole más potencia. Pero eso solamente era una muestra muy básica de las posibilidades. Con las versiones 8 y 9 de C# se añadieron muchas más posibilidades avanzadas que nos permiten dotar de más potencia y expresividad a nuestras estructuras switch, de maneras antes impensables.

Todo lo explicado en este artículo está disponible tan solo en .NET Core o .NET, pero no en .NET tradicional, ya que C# 8 apareció con la primera versión de .NET Core y ya no está disponible para la plataforma "clásica".

Vamos a repasar una a una las características de coincidencia de patrones en el switch.

Uso de expresiones en el switch

Una de las situaciones más habituales cuando empleamos un switch es que asigne un valor a una variable en función de un valor, siendo necesaria tan solo una instrucción de asignación.

Por ejemplo, este sería un código habitual:

string respuesta = "";

Persona persona = new Persona () {
    Nombre = "Rubén",
    Apellidos = "Rubio",
    TipoPersona = TipoPersona.Profesor
};

switch (persona.TipoPersona) {
    case TipoPersona.Alumno:
        respuesta = "Tipo alumno";
        break;
    case TipoPersona.Conserje:
        respuesta = "Tipo conserje";
        break;
    case TipoPersona.Profesor:
        respuesta = "Tipo profesor";
        break;
    case TipoPersona.Rector:
        respuesta = "Tipo rector";
        break;
    case TipoPersona.Secretario:
        respuesta = "Tipo secretario";
        break;
    case TipoPersona.Vigilante:
        respuesta = "Tipo vigilante";
        break;
    default:
        respuesta = "Desconocido";
        break;
}

Console.WriteLine ($"Valor con switch normal: {respuesta}");

Si utilizásemos expresiones eliminaríamos la necesidad de ciertos elementos normalmente presentes en el switch como son las palabras reservadas case, default y break.

Para aplicar expresiones y simplificar todo esto, debemos seguir los siguientes puntos:

  1. case: junto con los dos puntos lo reemplazaremos por los ya conocidos para el empleo de expresiones => que indican el inicio de la expresión.
  2. break: al tratarse una expresión, desaparece la necesidad de indicar el final de la misma, por lo que se hace innecesario su uso. Ahora para marcar el final de la expresión lo marcaremos con una coma al final de la misma.
  3. default: se sustituirá por el símbolo de descarte _.
  4. Por último intercambiaremos el orden entre la variable que marca la opción a seleccionar y la palabra reservada switch.
  5. El switch devolverá el resultado producido por la expresión. Además como el switch en sí mismo es una expresión, deberemos finalizar después de la última llave con punto y coma.

Así, podríamos reescribir el código anterior mediante el empleo de este patrón, y nos quedaría lo siguiente:

Persona persona = new Persona () {
    Nombre = "Rubén",
    Apellidos = "Rubio",
    TipoPersona = TipoPersona.Profesor
};

string respuesta = persona.TipoPersona switch
{
    TipoPersona.Alumno => "Tipo alumno",
    TipoPersona.Conserje => "Tipo conserje",
    TipoPersona.Profesor => "Tipo profesor",
    TipoPersona.Rector => "Tipo rector",
    TipoPersona.Secretario => "Tipo secretario",
    TipoPersona.Vigilante => "Tipo vigilante",
    _ => "Desconocido"
};

Console.WriteLine ($"Valor con switch de expresión: {respuesta}");

Fíjate en cómo se reduce notablemente el tamaño del código al eliminar todo lo superfluo, mientras que la ejecución sigue devolviendo el mismo resultado como se puede apreciar en la siguiente captura:

Ejecución con switch de expresión

Aplicado en propiedades

En situaciones como la que hemos visto antes, donde realmente comparamos con una propiedad del objeto, podemos dejar más limpio el código empleando esta característica que nos permite, para decidir la expresión a emplear, que especifiquemos la propiedad que tiene que tener ese valor.

Para hacerlo bastará con indicar entre llaves la propiedad y el valor separados por dos puntos, ubicándolo en el mismo sitio que antes poníamos únicamente el valor.

Lo vamos a ilustrar modificando el método anterior ObtenerTipoPersona.

private static string ObtenerTipoPersona (Persona persona) =>
    persona switch
    {
        { TipoPersona : TipoPersona.Alumno } => "Tipo alumno",
        { TipoPersona : TipoPersona.Conserje } => "Tipo conserje",
        { TipoPersona : TipoPersona.Profesor } => "Tipo profesor",
        { TipoPersona : TipoPersona.Rector } => "Tipo rector",
        { TipoPersona : TipoPersona.Secretario } => "Tipo secretario",
        { TipoPersona : TipoPersona.Vigilante } => "Tipo vigilante",
    _ => "Desconocido"
};

Si la observamos detenidamente, se puede comprobar que lo único que se altera es que pasamos el objeto completo (antes del switch se escribe únicamente persona y no persona.TipoPersona) y se altera lo que aparece a la izquierda de => para indicar también la propiedad sobre la que se aplica.

En tuplas

En el empleo de este patrón, se nos presenta una situación especial, que no deja de resolverse como hemos visto ya anteriormente, pero aplicado a este patrón. Se trata del caso en que se recibe una tupla como dato.

Tendremos que proporcionar una tupla completa para que se pueda aplicar la comparación. A partir de esa tupla podrá aplicar la comparación de la manera habitual en este tipo de datos.

El único matiz a tener en cuenta, es la situación donde no nos importe el valor de algunos elementos de la tupla, lo que a su vez nos permitirá crear el valor default. Para esto recurriremos al elemento descarte _.

Para ilustrarlo vamos a crear un switch que recibirá una tupla con el tipo de persona y aula y devolverá un valor diferente en función de una de las siguientes casuísticas:

  • Si es un profesor de cálculo devuelve "Profesor de cálculo".
  • Si es un profesor de álgebra devuelve "Profesor de álgebra".
  • Si es un alumno de cálculo devuelve "Alumno de cálculo".
  • Si es un alumno de álgebra devuelve "Alumno de álgebra".
  • Si es el rector devuelve "Es el rector" independientemente de la asignatura.
  • En el resto de los casos devuelve "Indiferente".
Profesor persona = new Profesor () {
    Nombre = "Rubén",
    Apellidos = "Rubio",
    TipoPersona = TipoPersona.Profesor,
    Asignatura = "Cálculo"
};

string respuesta = (persona.TipoPersona, persona.Asignatura) switch {
    (TipoPersona.Profesor, "Cálculo") => "Profesor de cálculo",
    (TipoPersona.Profesor, "Álgebra") => "Profesor de álgebra",
    (TipoPersona.Alumno, "Cálculo") => "Profesor de cálculo",
    (TipoPersona.Alumno, "Álgebra") => "Profesor de álgebra",
    (TipoPersona.Rector, _) => "Es el rector",
    (_, _) => "Indiferente"
};

Console.WriteLine ($"Valor con switch y tupla: {respuesta}");

El resultado en ambos casos es el mismo.

Ejecución con tuplas

Empleo posicional

Esta última opción se aprovecha de la capacidad de deconstrucción de nuestras clases. Consiste en pasar un objeto al switch para que posteriormente el switch lo deconstruya en una tupla, que será lo que empleemos para indicar qué expresión tiene que ejecutarse.

Para verlo podremos añadir a nuestras clases un método deconstructor que devuelva el tipo de persona y la asignatura y/o aula, lo que nos permitirá realizar el ejemplo del punto anterior pero pasando directamente el objeto:

// Clase Persona
public virtual void Deconstruct(out TipoPersona tipoPersona, out string asignatura)
{
    tipoPersona = TipoPersona;
    asignatura = "";
}

// Clase Profesor
public override void Deconstruct(out TipoPersona tipoPersona, out string asignatura)
{
    tipoPersona = TipoPersona;
    asignatura = Asignatura;
}

// Clase Alumno
public override void Deconstruct(out TipoPersona tipoPersona, out string asignatura)
{
    tipoPersona = TipoPersona;
    asignatura = Aula;
}

Ahora que tenemos los deconstructores añadidos, vamos a modificar el método ObtenerTipoPersona para que realice la operación en función de un objeto recibido de tipo Persona:

private static string ObtenerTipoPersona (Persona persona) =>
    persona switch
    {
        (TipoPersona.Profesor, "Cálculo") => "Profesor de cálculo",
        (TipoPersona.Profesor, "Álgebra") => "Profesor de álgebra",
        (TipoPersona.Alumno, "Cálculo") => "Profesor de cálculo",
        (TipoPersona.Alumno, "Álgebra") => "Profesor de álgebra",
        (TipoPersona.Rector, _) => "Es el rector",
        (_, _) => "Indiferente"
    };

Patrón simple

Se trata de un patrón también basado en el switch, mediante el cual vamos a poder realizar acciones que dependan del tipo de objeto.

Para ilustrarlo vamos a crear un método que devuelva un texto diferente en función del tipo específico de Persona que llegue: Alumno, Conserje, Profesor o cualquier otra clase que herede de Persona:

private static string TipoPersona (Persona persona) =>
    persona switch
    {
        Alumno => "alumno",
        Profesor => "profesor",
        Conserje => "conserje",
        _ => "persona"
    };

Si ahora realizamos varias llamadas y mostramos el resultado, podremos comprobar que difiere dependiendo del tipo de objeto:

var alumno = new Alumno();
Console.WriteLine (TipoPersona(alumno));
var conserje = new Conserje();
Console.WriteLine (TipoPersona(conserje));
var profesor = new Profesor();
Console.WriteLine (TipoPersona(profesor));
var persona = new Persona();
Console.WriteLine (TipoPersona(persona));

Y el resultado de su ejecución:

Ejecución patrón simple

Patrón relacional

A partir del patrón anterior, tenemos la opción de especificar que se ejecute una acción según un "filtro" que apliquemos empleando operadores relacionales.

Para ello bastaría con indicar, separando con la partícula when, los operadores relacionales que queremos aplicar.

En primer lugar veamos cómo podríamos aplicar esto de manera que nos devolviese la nota en forma de texto de un objeto de tipo Alumno. Para ello vamos a considerar que la clase Alumno tiene una propiedad Nota de tipo decimal que almacena la nota con decimales:

private static string TipoPersona (Persona persona) =>
    persona switch
    {
        Alumno al when al.Nota >= 9 => "sobresaliente",
        Alumno al when al.Nota >= 7 => "notable",
        Alumno al when al.Nota >= 6 => "bien",
        Alumno al when al.Nota >= 5 => "suficiente",
        _ => "suspenso"
    };

Patrón lógico

De forma similar al patrón relacional, podríamos emplear operadores lógicos que nos permitan unir operaciones relacionales o bien realizar solo operaciones lógicas en sí mismas.

A diferencia de los operadores lógicos que usamos habitualmente en condiciones y expresiones booleanas (&&, ||, !), en este caso se escriben como textos para diferenciarlos, de forma que utilizaremos: and, or y not.

En este caso las operaciones no se aplican solo con el switch, sino que podremos también emplearlos con la partícula if.

Veamos dos ejemplos de uso:

  • Empleado con switch:
private static string TipoPersona (decimal nota) =>
    nota switch
    {
        < 5 => "suspenso",
        >= 5 and < 6 => "suficiente",
        >= 6 and < 7 => "bien",
        >= 7 and < 9 => "notable",
        >= 9 => "sobresaliente"
    };
  • Empleado con if:
// Tal como haríamos tradicionalmente
if(!(alumno is Alumno))
{
    // Código a ejecutar si no se trata de un objeto de tipo Alumno
}

// Con el patrón lógico
if(alumno is not Alumno)
{
    // Código a ejecutar si NO se trata de un objeto de tipo Alumno
}

Unión y anidación de patrones

Pero todo esto que hemos estado viendo de forma separada, hay que pensar que es un todo y podemos combinar diferentes métodos e incluso anidar switch.

Veamos un ejemplo en el que se den las siguientes posibilidades:

  • Si se trata de un objeto cuya propiedad TipoPersona es: Conserje, Rector, - y/o Vigilante, que muestre el texto "Tipo XXXXX" tal y como se hizo en ejemplos de arriba.

  • Si se trata de un alumno:

    1. Con más de 4 asignaturas, que muestre su nombre y el número de asignaturas
    2. En caso contrario que muestre su nota media en texto
  • Si se trata de un profesor, que muestre si, según su edad, es novato (entre 18 y 24), experimentado (25 a 45) o eminencia (más de 45).

  • Si no cumple nada de lo anterior que muestre "Desconocido"

private static string ObtenerTipoPersona (Persona persona) =>
    persona switch
    {
        { TipoPersona : TipoPersona.Conserje } => "Tipo conserje",
        { TipoPersona : TipoPersona.Rector } => "Tipo rector",
        { TipoPersona : TipoPersona.Secretario } => "Tipo secretario",
        { TipoPersona : TipoPersona.Vigilante } => "Tipo vigilante",
        Profesor p => p.Edad switch
        {
            >= 18 and < 25 => "Profesor novato",
            >= 25 and <= 45 => "Profesor experimentado",
            > 45 => "Profesor eminencia",
            _ => "No puede ser un profesor"
        },
        Alumno al when al.ListaAsignaturas.Length > 4 => al.Nombre + "" + al.ListaAsignaturas.Length.ToString (),
        Alumno al => al.Nota switch
        {
            < 5 => "suspenso",
            >= 5 and < 6 => "suficiente",
            >= 6 and < 7 => "bien",
            >= 7 and < 9 => "notable",
            >= 9 => "sobresaliente"
        },
        _ => "Desconocido"
    };

A la hora de combinar diferentes métodos, debes tener siempre presente que es un switch, es decir, entrará por la primera opción disponible, así que es muy importante el orden que utilices.

En el caso de switch anidados, si entra en la opción desde el primero, el segundo deberá tener una opción válida si no queremos tener un error de ejecución; dicha opción puede ser un valor por defecto a través de un elemento de descarte (_).

Rubén Rubio Rubén lleva muchos años trabajando como desarrollador de software con diversas tecnologías y certificado por Microsoft desde el año 2003 en desarrollo con .NET. Ha trabajado como analista, desarrollador y responsable de TI en empresas de diferentes sectores. Actualmente trabaja como consultor y desarrollador independiente, ofreciendo sus servicios a empresas e instituciones, siendo también autor y docente en campusMVP. Ver todos los posts de Rubén Rubio
Archivado en: Lenguajes y plataformas

¿Te ha gustado este post?
Pues espera a ver nuestro boletín mensual...

Suscríbete a la newsletter

La mejor formación online para desarrolladores como tú

Agregar comentario

Los datos anteriores se utilizarán exclusivamente para permitirte hacer el comentario y, si lo seleccionas, notificarte de nuevos comentarios en este artículo, pero no se procesarán ni se utilizarán para ningún otro propósito. Lee nuestra política de privacidad.