Tutorial: Conceptos básicos de coincidencia de patrones

Tip

¿No está familiarizado con el desarrollo de software? Comience primero con los tutoriales de introducción . Presentan clases, métodos y flujo de control.

¿Competente en otro idioma? En este tutorial se muestra cómo los patrones de C# pueden expresar claramente el comportamiento de objetos cuando las reglas dependen del estado actual de un objeto.

En este tutorial, creará una aplicación de consola que modela las reglas de una esclusa de canal.

Una esclusa de canal eleva o desciende los barcos entre dos tramos de agua a diferentes alturas. Tiene dos compuertas y una cámara cuyo nivel de agua cambia entre una configuración baja y una configuración alta. La esclusa solo puede operar de manera segura cuando el nivel del agua y las posiciones de las compuertas se mantienen en combinaciones válidas.

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

  • Exprese el comportamiento del objeto haciendo coincidencia por el estado.
  • Implemente esas reglas con coincidencia de patrones de C#.
  • Utiliza diagnósticos del compilador para validar la implementación.

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".

Compilación de una simulación de una esclusa de canal

Una esclusa de canal eleva y desciende los barcos entre vías fluviales a diferentes niveles. En este tutorial, la esclusa simulada tiene una compuerta inferior, una compuerta superior y agua que puede estar baja o alta.

En funcionamiento normal, un barco entra cuando el nivel de agua dentro de la esclusa coincide con el nivel en el lado de entrada. Una vez que el barco está dentro, ambas puertas se cierran. El nivel de agua cambia para que coincida con el lado de salida y, a continuación, se abre la puerta de salida. Para mantener el modelo seguro, el nivel de agua solo puede cambiar cuando ambas puertas están cerradas, y una puerta solo puede abrirse cuando el nivel de agua coincide con el de ese lado.

Puede modelar esas reglas con una CanalLock clase . Expone comandos para abrir o cerrar cualquiera de las compuertas y para elevar o bajar el agua. También expone propiedades que informan del estado actual del bloqueo.

Define la clase

Cree un proyecto de consola y agregue una clase denominada CanalLock. Empiece por diseñar la API pública y dejar los métodos sin implementar:

public enum WaterLevel
{
    Low,
    High
}
public class CanalLock
{
    // Query canal lock state:
    public WaterLevel CanalLockWaterLevel { get; private set; } = WaterLevel.Low;
    public bool HighWaterGateOpen { get; private set; } = false;
    public bool LowWaterGateOpen { get; private set; } = false;

    // Change the upper gate.
    public void SetHighGate(bool open)
    {
        throw new NotImplementedException();
    }

    // Change the lower gate.
    public void SetLowGate(bool open)
    {
        throw new NotImplementedException();
    }

    // Change water level.
    public void SetWaterLevel(WaterLevel newLevel)
    {
        throw new NotImplementedException();
    }

    public override string ToString() =>
        $"The lower gate is {(LowWaterGateOpen ? "Open" : "Closed")}. " +
        $"The upper gate is {(HighWaterGateOpen ? "Open" : "Closed")}. " +
        $"The water level is {CanalLockWaterLevel}.";
}

El código anterior inicializa la esclusa con ambas puertas cerradas y el nivel de agua bajo. A continuación, agregue el código siguiente a Main para guiar su primera implementación:

// Create a new canal lock:
var canalGate = new CanalLock();

// State should be doors closed, water level low:
Console.WriteLine(canalGate);

canalGate.SetLowGate(open: true);
Console.WriteLine($"Open the lower gate:  {canalGate}");

Console.WriteLine("Boat enters lock from lower gate");

canalGate.SetLowGate(open: false);
Console.WriteLine($"Close the lower gate:  {canalGate}");

canalGate.SetWaterLevel(WaterLevel.High);
Console.WriteLine($"Raise the water level: {canalGate}");

canalGate.SetHighGate(open: true);
Console.WriteLine($"Open the higher gate:  {canalGate}");

Console.WriteLine("Boat exits lock at upper gate");
Console.WriteLine("Boat enters lock from upper gate");

canalGate.SetHighGate(open: false);
Console.WriteLine($"Close the higher gate: {canalGate}");

canalGate.SetWaterLevel(WaterLevel.Low);
Console.WriteLine($"Lower the water level: {canalGate}");

canalGate.SetLowGate(open: true);
Console.WriteLine($"Open the lower gate:  {canalGate}");

Console.WriteLine("Boat exits lock at upper gate");

canalGate.SetLowGate(open: false);
Console.WriteLine($"Close the lower gate:  {canalGate}");

Ahora agregue una primera implementación que cambie cada valor de estado sin aplicar las reglas de seguridad:

// Change the upper gate.
public void SetHighGate(bool open)
{
    HighWaterGateOpen = open;
}

// Change the lower gate.
public void SetLowGate(bool open)
{
    LowWaterGateOpen = open;
}

// Change water level.
public void SetWaterLevel(WaterLevel newLevel)
{
    CanalLockWaterLevel = newLevel;
}

Estas primeras comprobaciones pasan. Ya tiene la mecánica funcionando. A continuación, agregue una prueba para la primera condición de error. Al final de la secuencia anterior, ambas puertas están cerradas y el nivel de agua es bajo. Intente abrir la puerta superior:

Console.WriteLine("=============================================");
Console.WriteLine("     Test invalid commands");
// Open "wrong" gate (2 tests)
try
{
    canalGate = new CanalLock();
    canalGate.SetHighGate(open: true);
}
catch (InvalidOperationException)
{
    Console.WriteLine("Invalid operation: Can't open the high gate. Water is low.");
}
Console.WriteLine($"Try to open upper gate: {canalGate}");

Esa prueba falla porque la puerta superior se abre cuando no debería. Una primera corrección podría tener este aspecto:

// Change the upper gate.
public void SetHighGate(bool open)
{
    if (open && (CanalLockWaterLevel == WaterLevel.High))
        HighWaterGateOpen = true;
    else if (open && (CanalLockWaterLevel == WaterLevel.Low))
        throw new InvalidOperationException("Cannot open high gate when the water is low");
}

Sus pruebas pasan de nuevo. Pero a medida que se agregan más condiciones, se acumulan más if sentencias. El código es más difícil de examinar porque cada regla está separada de las demás.

Implementación de los comandos con patrones

Una opción más clara es usar patrones para describir directamente las combinaciones válidas. En el paso siguiente, cada expresión switch usa una tupla como entrada de patrón. C# evalúa esa tupla una vez, y cada caso del switch prueba el estado actual de la compuerta, el nivel de agua y la nueva configuración solicitada.

Para la puerta superior, puede resumir esas combinaciones así:

Nueva configuración Estado de la compuerta Nivel de agua Result
Cerrado Cerrado Alto Cerrado
Cerrado Cerrado Low Cerrado
Cerrado Open Alto Cerrado
Closed Abrir Bajo Closed
Open Cerrado Alto Open
Open Cerrado Low Cerrado (error)
Open Open Alto Open
Abrir Abrir Bajo Cerrada (error)

Las filas tachadas representan estados internos no válidos. La expresión switch puede codificar directamente las transiciones válidas. false significa que la puerta está cerrada:

HighWaterGateOpen = (open, HighWaterGateOpen, CanalLockWaterLevel) switch
{
    (false, false, WaterLevel.High) => false,
    (false, false, WaterLevel.Low) => false,
    (false, true, WaterLevel.High) => false,
    (false, true, WaterLevel.Low) => false, // should never happen
    (true, false, WaterLevel.High) => true,
    (true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the water is low"),
    (true, true, WaterLevel.High) => true,
    (true, true, WaterLevel.Low) => false, // should never happen
};

Pruebe esta versión. Las pruebas se realizan correctamente. El compilador también advierte de que la expresión switch no es exhaustiva, no cubre todos los valores posibles, porque WaterLevel es una enumeración y C# permite que cualquier valor del tipo numérico subyacente de la enumeración se convierta en esa enumeración, incluso uno sin un miembro con nombre. Agregue un brazo final con el patrón de descarte (_) para controlar esos estados internos imposibles:

_ => throw new InvalidOperationException("Invalid internal state"),

Ese brazo debe ser el último porque el patrón de descarte coincide con cualquier entrada restante.

A continuación, puede simplificar los brazos anteriores. El cierre de la compuerta siempre está permitido, por lo que un solo brazo puede reemplazar los cuatro casos cerrados independientes:

(false, _, _) => false,

También puede combinar los casos abiertos válidos y conservar el único error de seguridad:

(true, _, WaterLevel.High) => true,
(true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the water is low"),
_ => throw new InvalidOperationException("Invalid internal state"),

Vuelva a ejecutar el programa. Las pruebas aún pasan. Esta es la implementación final SetHighGate :

// Change the upper gate.
public void SetHighGate(bool open)
{
    HighWaterGateOpen = (open, HighWaterGateOpen, CanalLockWaterLevel) switch
    {
        (false, _,    _)               => false,
        (true, _,     WaterLevel.High) => true,
        (true, false, WaterLevel.Low)  => throw new InvalidOperationException("Cannot open high gate when the water is low"),
        _                              => throw new InvalidOperationException("Invalid internal state"),
    };
}

Implementar las reglas restantes

Ahora aplique la misma idea a SetLowGate y SetWaterLevel. Empiece agregando pruebas que expongan operaciones no válidas:

Console.WriteLine();
Console.WriteLine();
try
{
    canalGate = new CanalLock();
    canalGate.SetWaterLevel(WaterLevel.High);
    canalGate.SetLowGate(open: true);
}
catch (InvalidOperationException)
{
    Console.WriteLine("invalid operation: Can't open the lower gate. Water is high.");
}
Console.WriteLine($"Try to open lower gate: {canalGate}");
// change water level with gate open (2 tests)
Console.WriteLine();
Console.WriteLine();
try
{
    canalGate = new CanalLock();
    canalGate.SetLowGate(open: true);
    canalGate.SetWaterLevel(WaterLevel.High);
}
catch (InvalidOperationException)
{
    Console.WriteLine("invalid operation: Can't raise water when the lower gate is open.");
}
Console.WriteLine($"Try to raise water with lower gate open: {canalGate}");
Console.WriteLine();
Console.WriteLine();
try
{
    canalGate = new CanalLock();
    canalGate.SetWaterLevel(WaterLevel.High);
    canalGate.SetHighGate(open: true);
    canalGate.SetWaterLevel(WaterLevel.Low);
}
catch (InvalidOperationException)
{
    Console.WriteLine("invalid operation: Can't lower water when the high gate is open.");
}
Console.WriteLine($"Try to lower water with high gate open: {canalGate}");

Ejecute la aplicación de nuevo. Estas pruebas fallan y la esclusa alcanza estados no válidos. Implemente los métodos restantes mediante la coincidencia en el estado completo. SetLowGate es similar a SetHighGate. SetWaterLevel utiliza el nivel de agua actual más las dos posiciones de la puerta:

CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
{
    // arms go here
};

Tienes 16 combinaciones que tener en cuenta. Comience con la tabla completa, escriba las ramas del switch, ejecute las pruebas y, a continuación, simplifique los resultados repetidos.

¿Terminaste usando métodos similares a estos?

// Change the lower gate.
public void SetLowGate(bool open)
{
    LowWaterGateOpen = (open, LowWaterGateOpen, CanalLockWaterLevel) switch
    {
        (false, _, _) => false,
        (true, _, WaterLevel.Low) => true,
        (true, false, WaterLevel.High) => throw new InvalidOperationException("Cannot open low gate when the water is high"),
        _ => throw new InvalidOperationException("Invalid internal state"),
    };
}

// Change water level.
public void SetWaterLevel(WaterLevel newLevel)
{
    CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
    {
        (WaterLevel.Low, WaterLevel.Low, true, false) => WaterLevel.Low,
        (WaterLevel.High, WaterLevel.High, false, true) => WaterLevel.High,
        (WaterLevel.Low, _, false, false) => WaterLevel.Low,
        (WaterLevel.High, _, false, false) => WaterLevel.High,
        (WaterLevel.Low, WaterLevel.High, false, true) => throw new InvalidOperationException("Cannot lower water when the high gate is open"),
        (WaterLevel.High, WaterLevel.Low, true, false) => throw new InvalidOperationException("Cannot raise water when the low gate is open"),
        _ => throw new InvalidOperationException("Invalid internal state"),
    };
}

Las pruebas ahora deberían pasar y la esclusa debería aplicar sus reglas de seguridad.

Summary

En este tutorial, ha usado la coincidencia de patrones para expresar cómo un objeto puede cambiar de un estado válido a otro. Los patrones mantienen juntas las transiciones permitidas para poder compararlas con mayor facilidad que con una larga serie de instrucciones de bifurcación. Este enfoque funciona bien cuando el comportamiento depende de la forma combinada de varios valores de estado.