Tutorial: Nozioni di base sul pattern matching

Tip

Novità dello sviluppo di software? Iniziare prima con le esercitazioni introduttive . Introducono classi, metodi e flusso di controllo.

Esperienza in un'altra lingua? Questa esercitazione illustra in che modo i pattern C# possono esprimere chiaramente il comportamento degli oggetti quando le regole dipendono dallo stato corrente di un oggetto.

In questa esercitazione si crea un'app console che modella le regole per una chiusa di canale.

Una chiusa alza o abbassa le barche tra due tratti d'acqua a altezze diverse. Ha due cancelli e una camera con un livello dell'acqua che cambia tra un livello basso e un livello alto. La chiusa può funzionare in modo sicuro solo quando il livello dell'acqua e le posizioni delle paratoie rimangono in combinazioni valide.

In questa esercitazione apprenderai a:

  • Esprimi il comportamento dell'oggetto confrontando lo stato.
  • Implementa queste regole con il pattern matching di C#.
  • Usare la diagnostica del compilatore per convalidare l'implementazione.

Prerequisites

Istruzioni per l'installazione

Su Windows, usa questo file di configurazione WinGet per installare tutti i prerequisiti. Se è già installato un elemento, WinGet ignorerà questo passaggio.

  1. Scaricare il file e fare doppio clic per eseguirlo.
  2. Leggere il contratto di licenza, digitare ye selezionare Immettere quando viene richiesto di accettare.
  3. Se viene visualizzato un prompt di controllo dell'account utente lampeggiante nella barra delle applicazioni, consentire all'installazione di continuare.

In altre piattaforme è necessario installare ognuno di questi componenti separatamente.

  1. Scaricare il programma di installazione consigliato dalla pagina di download .NET SDK e fare doppio clic per eseguirlo. La pagina di download rileva la piattaforma e consiglia il programma di installazione più recente per la piattaforma.
  2. Scaricare il programma di installazione più recente dalla home page Visual Studio Code e fare doppio clic per eseguirlo. Questa pagina rileva anche la tua piattaforma e il collegamento dovrebbe essere corretto per il tuo sistema.
  3. Fare clic sul pulsante "Installa" nella pagina dell'estensione C# DevKit. Si apre Visual Studio Code e ti chiede se vuoi installare o abilitare l'estensione. Seleziona "install".

Creare una simulazione di una chiusa del canale

Una chiusa alza e abbassa le barche tra corsi d'acqua a livelli diversi. In questa esercitazione la chiusa simulata ha una porta inferiore, una porta superiore e un'acqua che può essere bassa o alta.

Nel normale funzionamento, una barca entra quando il livello dell'acqua all'interno della chiusa corrisponde al livello sul lato ingresso. Una volta che la barca è dentro, entrambe le paratoie si chiudono. Il livello dell'acqua cambia in modo che corrisponda al lato uscita e quindi si apre il cancello di uscita. Per mantenere il modello sicuro, il livello dell'acqua può cambiare solo quando entrambe le porte sono chiuse e una porta può aprirsi solo quando il livello dell'acqua corrisponde a quel lato.

È possibile modellare tali regole con una CanalLock classe . Espone i comandi per aprire o chiudere uno dei due cancelli e per sollevare o abbassare l'acqua. Espone anche le proprietà che segnalano lo stato corrente del blocco.

Definire la classe

Creare un progetto console e quindi aggiungere una classe denominata CanalLock. Iniziare progettando l'API pubblica e lasciando i metodi non implementati:

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

Il codice precedente inizializza la chiusa con entrambi i cancelli chiusi e il livello dell'acqua basso. Aggiungere quindi il codice seguente a Main per guidare la prima implementazione:

// 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}");

Aggiungere ora una prima implementazione che modifica ogni valore di stato senza applicare le regole di sicurezza:

// 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;
}

Questi primi controlli passano. Hai i meccanici al lavoro. Aggiungere quindi un test per la prima condizione di errore. Alla fine della sequenza precedente, entrambe le paratoie sono chiuse e il livello dell'acqua è basso. Prova ad aprire il cancello in alto:

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}");

Il test ha esito negativo perché il cancello superiore si apre quando non dovrebbe. Una prima correzione potrebbe essere così:

// 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");
}

I tuoi test passano di nuovo. Tuttavia, man mano che si aggiungono altre condizioni, si accumulano più if istruzioni. Il codice diventa più difficile da analizzare perché ogni regola è separata dalle altre.

Implementa i comandi con i modelli

Un'opzione più chiara consiste nell'usare i pattern per descrivere direttamente le combinazioni valide. Nel passaggio successivo ogni espressione switch usa una tupla come input del pattern. C# valuta la tupla una sola volta e ogni ramo dello switch verifica lo stato corrente del gate, il livello dell'acqua e la nuova impostazione richiesta.

Per il cancello superiore, è possibile riepilogare queste combinazioni come segue:

Nuova impostazione Stato del gate Livello dell'acqua Result
Chiuso Chiuso Alto Chiuso
Chiuso Chiuso Low Chiuso
Chiuso Open Alto Chiuso
Chiuso Si apra Basso Chiuso
Open Chiuso Alto Open
Open Chiuso Low Chiuso (errore)
Open Open Alto Open
Si apra Si apra Basso Chiuso (errore)

Le righe barrate rappresentano stati interni non validi. L'espressione switch può codificare direttamente le transizioni valide. false significa comunque che il cancello è chiuso:

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
};

Prova questa versione. I test sono superati. Il compilatore avvisa inoltre che l'espressione switch non è esaustiva , che non copre ogni valore possibile, perché WaterLevel è un'enumerazione e C# consente di eseguire il cast di qualsiasi valore del tipo numerico sottostante dell'enumerazione, anche uno senza un membro denominato. Aggiungere un braccio finale con il modello di eliminazione (_) per gestire gli stati interni impossibili:

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

Quel ramo deve essere l'ultimo perché il pattern discard corrisponde a ogni input rimanente.

È quindi possibile semplificare i rami precedenti. La chiusura della porta logica è sempre consentita, quindi un ramo può sostituire i quattro casi chiusi separati:

(false, _, _) => false,

È anche possibile combinare i casi aperti validi e mantenere l'unico errore di sicurezza:

(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"),

Eseguire di nuovo il programma. I test passano ancora. Ecco l'implementazione finale 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"),
    };
}

Implementa le regole rimanenti

Applicare ora la stessa idea a SetLowGate e SetWaterLevel. Per iniziare, aggiungere test che espongono operazioni non valide:

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}");

Eseguire di nuovo l'app. Questi test hanno esito negativo e la chiusa del canale raggiunge stati non validi. Implementare i metodi rimanenti mediante il matching sullo stato completo. SetLowGate è simile a SetHighGate. SetWaterLevel utilizza il livello corrente dell'acqua più entrambe le posizioni delle paratoie:

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

Sono disponibili 16 combinazioni da considerare. Iniziare con la tabella completa, scrivere i rami dello switch, eseguire i test e quindi semplificare i risultati ripetuti.

Ti sei ritrovato con metodi simili a questi?

// 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"),
    };
}

Le prove dovrebbero ora essere superate e la chiusa del canale dovrebbe applicare le sue regole di sicurezza.

Sommario

In questa esercitazione sono stati usati pattern matching per esprimere il modo in cui un oggetto può passare da uno stato valido a un altro. I pattern mantengono insieme le transizioni consentite in modo da poterle confrontare più facilmente rispetto a una lunga serie di istruzioni di diramazione. Questo approccio funziona correttamente quando il comportamento dipende dalla forma combinata di diversi valori di stato.