Tutorial: Creación de algoritmos mediante la coincidencia de patrones

La coincidencia de patrones permite agregar funcionalidad para los tipos definidos en otras bibliotecas sin modificar esos tipos. Los patrones también se usan para crear una funcionalidad que la aplicación requiere y que no es una característica fundamental del tipo que se está ampliando.

En este tutorial, obtendrá información sobre cómo:

  • Reconocer situaciones donde se debe usar la coincidencia de patrones.
  • Usar las expresiones de coincidencia de patrones para implementar un comportamiento en función de los tipos y los valores de propiedad.
  • Combinar la coincidencia de patrones con otras técnicas para crear algoritmos completos.

Prerequisites

Instrucciones de instalación

En Windows, este archivo de configuración WinGet se utiliza para instalar todos los requisitos previos. Si ya tiene algo instalado, WinGet omitirá ese paso.

  1. Descargue el archivo y haga doble clic para ejecutarlo.
  2. Lea el contrato de licencia, escriba yy seleccione Escriba cuando se le pida que acepte.
  3. Si recibe un mensaje de control de cuentas de usuario (UAC) parpadeante en la barra de tareas, permita que la instalación continúe.

En otras plataformas, debe instalar cada uno de estos componentes por separado.

  1. Descargue el instalador recomendado en la página de descarga del SDK .NET y haga doble clic para ejecutarlo. La página de descarga detecta la plataforma y recomienda el instalador más reciente para la plataforma.
  2. Descargue el instalador más reciente de la página principal Visual Studio Code y haga doble clic para ejecutarlo. Esa página también detecta tu plataforma y el vínculo debe ser correcto para tu sistema operativo.
  3. Haga clic en el botón "Instalar" de la página de extensión C# DevKit. Esto abre Visual Studio código y le pregunta si desea instalar o habilitar la extensión. Seleccione "instalar".

Escenarios para la coincidencia de patrones

Con frecuencia, el desarrollo moderno incluye integrar datos desde varios orígenes y presentar información y perspectivas a partir de esos datos en una sola aplicación cohesiva. Es posible que usted y su equipo no controlen los tipos que representan los datos entrantes.

El enfoque de diseño orientado a objetos clásico requiere crear tipos en la aplicación que representan cada tipo de datos de esos varios orígenes de datos. A continuación, la aplicación funciona con esos nuevos tipos, construye jerarquías de herencia, crea métodos virtuales e implementa abstracciones. Esas técnicas funcionan y son a veces las mejores herramientas. En otras ocasiones, puede escribir menos código. En algunos casos, separar los datos de las operaciones que los usan puede facilitar la lectura del código.

En este tutorial, creará y explorará una aplicación que toma datos entrantes de varios orígenes externos para un único escenario. Verá cómo la coincidencia de patrones proporciona una manera eficaz de consumir y procesar esos datos de maneras que no formaban parte del sistema original.

Considere un área metropolitana importante que usa peajes y precios de hora punta para administrar el tráfico. Puede escribir una aplicación que calcule los peajes de un vehículo en función de su tipo. Mejoras posteriores incorporan precios basados en la cantidad de ocupantes del vehículo. Otras mejoras agregan precios según la hora y el día de la semana.

A partir de esa breve descripción, puede dibujar rápidamente una jerarquía de objetos para modelar este sistema. Sin embargo, los datos proceden de varios orígenes, como otros sistemas de gestión de registros de vehículos. Estos sistemas proporcionan clases diferentes para modelar esos datos y esos sistemas no comparten un único modelo de objetos. En este tutorial, usará estas clases simplificadas para modelar los datos del vehículo de estos sistemas externos, como se muestra en el código siguiente:

namespace ConsumerVehicleRegistration
{
    public class Car
    {
        public int Passengers { get; set; }
    }
}

namespace CommercialRegistration
{
    public class DeliveryTruck
    {
        public int GrossWeightClass { get; set; }
    }
}

namespace LiveryRegistration
{
    public class Taxi
    {
        public int Fares { get; set; }
    }

    public class Bus
    {
        public int Capacity { get; set; }
        public int Riders { get; set; }
    }
}

Puede descargar el código de inicio del repositorio dotnet/samples de GitHub. Puede ver que las clases de vehículo son de diferentes sistemas y están en diferentes espacios de nombres. No se puede usar una clase base común que no sea System.Object.

Diseños de coincidencia de patrones

El escenario usado en este tutorial resalta los tipos de problemas que la coincidencia de patrones resuelve bien:

  • Los objetos con los que necesita trabajar no están en una jerarquía de objetos que coincida con sus objetivos. Es posible que esté trabajando con clases que forman parte de sistemas no relacionados.
  • La funcionalidad que agrega no forma parte de la abstracción central de estas clases. El peaje que paga un vehículo cambia según los distintos tipos de vehículos, pero el peaje no es una función central del vehículo.

Cuando la forma de los datos y las operaciones que se realizan en esos datos no se describen en conjunto, las características de coincidencia de patrones de C# permiten que sea más fácil trabajar con ellos.

Implementación de cálculos de peajes básicos

El cálculo de peaje más básico solo se basa en el tipo de vehículo:

  • Un Car es USD 2,00.
  • Un Taxi es USD 3,50.
  • Un Bus cuesta 5,00 USD.
  • Un DeliveryTruck cuesta 10,00 USD.

Cree una clase TollCalculator nueva e implemente la coincidencia de patrones en el tipo de vehículo para obtener el importe del peaje. En el siguiente código se muestra la implementación inicial de TollCalculator.

using System;
using CommercialRegistration;
using ConsumerVehicleRegistration;
using LiveryRegistration;

namespace Calculators;

public class TollCalculator
{
    public decimal CalculateToll(object vehicle) =>
        vehicle switch
    {
        Car c           => 2.00m,
        Taxi t          => 3.50m,
        Bus b           => 5.00m,
        DeliveryTruck t => 10.00m,
        { }             => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)),
        null            => throw new ArgumentNullException(nameof(vehicle))
    };
}

El código anterior usa una expresión switch (que no es lo mismo que una instrucción switch) que prueba el patrón de declaración. Una expresión switch comienza por la variable, vehicle en el código anterior, seguida de la palabra clave switch. A continuación, todos los segmentos modificadores aparecen entre llaves. La expresión switch lleva a cabo otras mejoras en la sintaxis que rodea la instrucción switch. La palabra clave case se omite y el resultado de cada segmento es una expresión. Los dos últimos segmentos muestran una característica de lenguaje nueva. El caso { } coincide con cualquier objeto no nulo que no coincidía con ningún segmento anterior. Este segmento detecta todo tipo incorrecto que se pasa a este método. El caso { } debe corresponder a los casos de cada tipo de vehículo. Si se invierte el orden, el caso { } tendrá prioridad. Por último, el nullpatrón de constante detecta si se pasa null a este método. El patrón null puede ser el último porque los otros patrones solo coinciden con un objeto no nulo del tipo correcto.

Puede probar este código usando el siguiente código en Program.cs:

using System;
using CommercialRegistration;
using ConsumerVehicleRegistration;
using LiveryRegistration;

using toll_calculator;

var tollCalc = new TollCalculator();

var car = new Car();
var taxi = new Taxi();
var bus = new Bus();
var truck = new DeliveryTruck();

Console.WriteLine($"The toll for a car is {tollCalc.CalculateToll(car)}");
Console.WriteLine($"The toll for a taxi is {tollCalc.CalculateToll(taxi)}");
Console.WriteLine($"The toll for a bus is {tollCalc.CalculateToll(bus)}");
Console.WriteLine($"The toll for a truck is {tollCalc.CalculateToll(truck)}");

try
{
    tollCalc.CalculateToll("this will fail");
}
catch (ArgumentException e)
{
    Console.WriteLine("Caught an argument exception when using the wrong type");
}
try
{
    tollCalc.CalculateToll(null!);
}
catch (ArgumentNullException e)
{
    Console.WriteLine("Caught an argument exception when using null");
}

Ese código se incluye en el proyecto de inicio, pero está comentado. Quite los comentarios y puede probar lo que escribió.

Empezará a ver cómo los patrones pueden ayudarlo a crear algoritmos cuando el código y los datos están separados. La expresión switch prueba el tipo y genera valores distintos en función de los resultados. Eso es solo el principio.

Incorporación de precios por ocupación

La autoridad encargada de los peajes quiere incentivar que los vehículos viajen a plena capacidad. Deciden cobrar más cuando los vehículos tienen menos pasajeros y fomentan vehículos completos al ofrecer precios más bajos:

  • Los automóviles y taxis sin pasajeros pagan USD 0,50 adicionales.
  • Los automóviles y taxis con dos pasajeros tienen un descuento de USD 0,50.
  • Los automóviles y taxis con tres o más pasajeros tienen un descuento de USD 1.
  • Los buses que viajan con menos del 50 % de su capacidad pagan USD 2 adicionales.
  • Los buses que viajan con más del 90 % de su capacidad tienen un descuento de USD 1.

Puede implementar estas reglas mediante un patrón property en la misma expresión switch. Un patrón de propiedad compara un valor de propiedad con un valor constante. El patrón de propiedad examina las propiedades del objeto una vez determinado el tipo. Car {Passengers: 0} es en sí mismo un patrón recursivo: el patrón de propiedad externa en Car contiene un patrón constante interno que prueba el Passengers valor. El caso único de Car se amplía a cuatro casos distintos:

vehicle switch
{
    Car {Passengers: 0} => 2.00m + 0.50m,
    Car {Passengers: 1} => 2.0m,
    Car {Passengers: 2} => 2.0m - 0.50m,
    Car                 => 2.00m - 1.0m,

    // ...
};

Los primeros tres casos prueban el tipo como Car y luego comprueban el valor de la propiedad Passengers. Si ambas coinciden, la expresión se evalúa y devuelve un resultado.

También puede expandir los casos de taxis de una manera similar:

vehicle switch
{
    // ...

    Taxi {Fares: 0}  => 3.50m + 1.00m,
    Taxi {Fares: 1}  => 3.50m,
    Taxi {Fares: 2}  => 3.50m - 0.50m,
    Taxi             => 3.50m - 1.00m,

    // ...
};

A continuación, implemente las reglas de ocupación ampliando los casos para los autobuses, tal como se muestra en el siguiente ejemplo.

vehicle switch
{
    // ...

    Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
    Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
    Bus => 5.00m,

    // ...
};

A la autoridad encargada de los peajes no le preocupa el número de pasajeros en los camiones de reparto. Alternativamente, ajustan el importe del peaje en base a la clase de peso de los camiones, como sigue:

  • A los camiones de más de 2268 kilos se les cobra $5.00 adicionales.
  • Los camiones livianos, por debajo de los 1360 kilos, tienen un descuento de 2 USD.

Puede implementar esa regla con el código siguiente:

vehicle switch
{
    // ...

    DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m,
    DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m,
    DeliveryTruck => 10.00m,
};

En el código anterior se muestra la cláusula when de un segmento modificador. Use la when cláusula para probar condiciones distintas de la igualdad en una propiedad. Cuando termine, tiene un método similar al código siguiente:

vehicle switch
{
    Car {Passengers: 0}        => 2.00m + 0.50m,
    Car {Passengers: 1}        => 2.0m,
    Car {Passengers: 2}        => 2.0m - 0.50m,
    Car                        => 2.00m - 1.0m,

    Taxi {Fares: 0}  => 3.50m + 1.00m,
    Taxi {Fares: 1}  => 3.50m,
    Taxi {Fares: 2}  => 3.50m - 0.50m,
    Taxi             => 3.50m - 1.00m,

    Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
    Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
    Bus => 5.00m,

    DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m,
    DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m,
    DeliveryTruck => 10.00m,

    { }     => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)),
    null    => throw new ArgumentNullException(nameof(vehicle))
};

Como se indicó anteriormente, estos brazos de switch son patrones recursivos, anidando un patrón constante dentro de un patrón de propiedad.

Puede usar modificadores anidados para que este código sea menos repetitivo. Tanto Car como Taxi tienen cuatro segmentos distintos en los ejemplos anteriores. En ambos casos, se puede crear un patrón de declaración que se alimenta de un patrón de constante. Esta técnica se muestra en el código siguiente:

public decimal CalculateToll(object vehicle) =>
    vehicle switch
    {
        Car c => c.Passengers switch
        {
            0 => 2.00m + 0.5m,
            1 => 2.0m,
            2 => 2.0m - 0.5m,
            _ => 2.00m - 1.0m
        },

        Taxi t => t.Fares switch
        {
            0 => 3.50m + 1.00m,
            1 => 3.50m,
            2 => 3.50m - 0.50m,
            _ => 3.50m - 1.00m
        },

        Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
        Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
        Bus b => 5.00m,

        DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m,
        DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m,
        DeliveryTruck t => 10.00m,

        { }  => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)),
        null => throw new ArgumentNullException(nameof(vehicle))
    };

En el ejemplo anterior, anidar una expresión switch dentro de otro brazo switch significa que no se repiten los brazos Car y Taxi que contienen brazos secundarios que prueban el valor de la propiedad. Esta técnica no se usa para los Bus brazos y DeliveryTruck brazos porque esos brazos prueban intervalos para la propiedad, no valores discretos.

Añadir tarifa de horas punta

Para la funcionalidad final, la autoridad de peaje quiere agregar tarificación en horas punta en función del tiempo. En las horas de mayor afluencia durante mañana y tarde, el valor de los peajes se dobla. Esa regla solo afecta el tráfico en una dirección: hacia la ciudad por la mañana y fuera de la ciudad en la hora punta de la tarde. En otros momentos durante la jornada laboral, los peajes aumentan en un 50 %. Por la noche y por la mañana, los peajes se reducen en un 25%. Durante el fin de semana, la tarifa es normal independientemente de la hora. Puede usar una serie de instrucciones if e instrucciones else para expresar esta regla mediante el código siguiente:

public decimal PeakTimePremiumIfElse(DateTime timeOfToll, bool inbound)
{
    if ((timeOfToll.DayOfWeek == DayOfWeek.Saturday) ||
        (timeOfToll.DayOfWeek == DayOfWeek.Sunday))
    {
        return 1.0m;
    }
    else
    {
        int hour = timeOfToll.Hour;
        if (hour < 6)
        {
            return 0.75m;
        }
        else if (hour < 10)
        {
            if (inbound)
            {
                return 2.0m;
            }
            else
            {
                return 1.0m;
            }
        }
        else if (hour < 16)
        {
            return 1.5m;
        }
        else if (hour < 20)
        {
            if (inbound)
            {
                return 1.0m;
            }
            else
            {
                return 2.0m;
            }
        }
        else // Overnight
        {
            return 0.75m;
        }
    }
}

El código anterior funciona correctamente, pero no es legible. Para que el código tenga sentido, tiene que encadenar todos los casos de entrada y las instrucciones if anidadas. En su lugar, utilizarás la coincidencia de patrones para esta función, pero la integrarás con otras técnicas. Puede crear una expresión de coincidencia de patrones única que tenga en cuenta todas las combinaciones de dirección, día de la semana y hora. El resultado sería una expresión complicada. Podría ser difícil de leer y de comprender. Esto implica que es difícil garantizar su exactitud. En su lugar, combine esos método para crear una tupla de valores que describa de manera concisa todos esos estados. Luego, use la coincidencia de patrones para calcular un multiplicador para el peaje. La tupla contiene tres condiciones discretas:

  • El día es un día laborable o fin de semana.
  • La franja horaria en la que se cobra el peaje.
  • La dirección es hacia la ciudad o fuera de la ciudad.

En la tabla siguiente se muestran las combinaciones de valores de entrada y el multiplicador de precio en horas punta:

Día Time Direction Premium
Día de la semana hora punta de la mañana inbound x 2,00
Día de la semana hora punta de la mañana outbound x 1,00
Día de la semana horas diurnas inbound x 1,50
Día de la semana horas diurnas outbound x 1,50
Día de la semana hora punta de la tarde inbound x 1,00
Día de la semana hora punta de la tarde outbound x 2,00
Día de la semana durante la noche inbound x 0,75
Día de la semana durante la noche outbound x 0,75
Fin de semana hora punta de la mañana inbound x 1,00
Fin de semana hora punta de la mañana outbound x 1,00
Fin de semana horas diurnas inbound x 1,00
Fin de semana horas diurnas outbound x 1,00
Fin de semana hora punta de la tarde inbound x 1,00
Fin de semana hora punta de la tarde outbound x 1,00
Fin de semana durante la noche inbound x 1,00
Fin de semana durante la noche outbound x 1,00

Hay 16 combinaciones distintas de las tres variables. Al combinar algunas de las condiciones, se simplifica la expresión switch final.

El sistema que cobra los peajes usa una estructura DateTime para la hora en que se cobró el peaje. Genere métodos de miembro que creen las variables a partir de la tabla anterior. La función siguiente usa una expresión switch de coincidencia de patrones para expresar si la estructura DateTime representa un día laborable o un fin de semana:

private static bool IsWeekDay(DateTime timeOfToll) =>
    timeOfToll.DayOfWeek switch
    {
        DayOfWeek.Monday    => true,
        DayOfWeek.Tuesday   => true,
        DayOfWeek.Wednesday => true,
        DayOfWeek.Thursday  => true,
        DayOfWeek.Friday    => true,
        DayOfWeek.Saturday  => false,
        DayOfWeek.Sunday    => false
    };

Ese método es correcto, pero es redundante. Puede simplificarlo tal como se muestra en el código siguiente:

private static bool IsWeekDay(DateTime timeOfToll) =>
    timeOfToll.DayOfWeek switch
    {
        DayOfWeek.Saturday => false,
        DayOfWeek.Sunday => false,
        _ => true
    };

A continuación, agregue una función similar para categorizar la hora en los bloques:

private enum TimeBand
{
    MorningRush,
    Daytime,
    EveningRush,
    Overnight
}

private static TimeBand GetTimeBand(DateTime timeOfToll) =>
    timeOfToll.Hour switch
    {
        < 6 or > 19 => TimeBand.Overnight,
        < 10 => TimeBand.MorningRush,
        < 16 => TimeBand.Daytime,
        _ => TimeBand.EveningRush,
    };

Agregue un elemento enum privado para convertir cada intervalo de tiempo en un valor discreto. A continuación, el GetTimeBand método usa patrones relacionales y patrones lógicos. El patrón relacional permite probar un valor numérico mediante <, >, <= o >=. Los patrones lógicos combinan otros patrones: un or patrón comprueba si una expresión coincide con uno o varios patrones, un and patrón prueba que una expresión coincide con dos patrones distintos y un not patrón prueba que una expresión no coincide con un patrón.

Después de crear esos métodos, puede usar otra switch expresión con un patrón de tupla (un patrón posicional que coincida con cada elemento de una tupla) para calcular el recargo de precio. Puede crear una expresión switch con los 16 brazos:

public decimal PeakTimePremiumFull(DateTime timeOfToll, bool inbound) =>
    (IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
    {
        (true, TimeBand.MorningRush, true) => 2.00m,
        (true, TimeBand.MorningRush, false) => 1.00m,
        (true, TimeBand.Daytime, true) => 1.50m,
        (true, TimeBand.Daytime, false) => 1.50m,
        (true, TimeBand.EveningRush, true) => 1.00m,
        (true, TimeBand.EveningRush, false) => 2.00m,
        (true, TimeBand.Overnight, true) => 0.75m,
        (true, TimeBand.Overnight, false) => 0.75m,
        (false, TimeBand.MorningRush, true) => 1.00m,
        (false, TimeBand.MorningRush, false) => 1.00m,
        (false, TimeBand.Daytime, true) => 1.00m,
        (false, TimeBand.Daytime, false) => 1.00m,
        (false, TimeBand.EveningRush, true) => 1.00m,
        (false, TimeBand.EveningRush, false) => 1.00m,
        (false, TimeBand.Overnight, true) => 1.00m,
        (false, TimeBand.Overnight, false) => 1.00m,
    };

El código anterior funciona, pero se puede simplificar. Todas las ocho combinaciones para el fin de semana tienen el mismo costo. Puede reemplazar las ocho por la siguiente línea:

(false, _, _) => 1.0m,

Tanto el tráfico hacia la ciudad como el tráfico desde la ciudad tienen el mismo multiplicador durante el día y la noche de los fines de semana. Puede reemplazar esos cuatro switch arms por las dos líneas siguientes:

(true, TimeBand.Overnight, _) => 0.75m,
(true, TimeBand.Daytime, _)   => 1.5m,

El código debe ser similar al código siguiente después de esos dos cambios:

public decimal PeakTimePremium(DateTime timeOfToll, bool inbound) =>
    (IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
    {
        (true, TimeBand.MorningRush, true)  => 2.00m,
        (true, TimeBand.MorningRush, false) => 1.00m,
        (true, TimeBand.Daytime,     _)     => 1.50m,
        (true, TimeBand.EveningRush, true)  => 1.00m,
        (true, TimeBand.EveningRush, false) => 2.00m,
        (true, TimeBand.Overnight,   _)     => 0.75m,
        (false, _,                   _)     => 1.00m,
    };

Por último, quite los dos tramos de hora punta con precio normal. Después de quitar esos brazos, reemplace false por un descarte (_) en la rama final del conmutador. Obtendrá el siguiente método completado:

public decimal PeakTimePremium(DateTime timeOfToll, bool inbound) =>
    (IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
    {
        (true, TimeBand.Overnight, _) => 0.75m,
        (true, TimeBand.Daytime, _) => 1.5m,
        (true, TimeBand.MorningRush, true) => 2.0m,
        (true, TimeBand.EveningRush, false) => 2.0m,
        _ => 1.0m,
    };

En este ejemplo se resalta una de las ventajas de la coincidencia de patrones: las ramas del patrón se evalúan en orden. Si vuelve a ordenarlas para que una rama anterior controle uno de los últimos casos, el compilador genera una advertencia sobre el código inaccesible. Esas reglas de lenguaje facilitan la realización de las simplificaciones anteriores con la confianza de que el código no cambió.

La coincidencia de patrones hace que algunos tipos de código sean más legibles y ofrece una alternativa a las técnicas orientadas a objetos cuando no se puede agregar código a las clases. La nube hace que los datos y la funcionalidad residan por separado. La forma de los datos y las operaciones que se realizan en ellos no necesariamente se describen en conjunto. En este tutorial, consumió datos existentes de maneras totalmente distintas de su función original. La coincidencia de patrones le ha brindado la capacidad de escribir una funcionalidad que reemplazase a esos tipos, aunque no le permitía extenderlos.

Pasos siguientes

Puede descargar el código finalizado del repositorio GitHub dotnet/samples. Explore los patrones por su cuenta y agregue esta técnica a sus actividades habituales de codificación. Aprender estas técnicas le permite contar con otra forma de enfocar los problemas y crear una funcionalidad nueva.

Consulte también