チュートリアル: パターン マッチングの基本

Tip

ソフトウェアの開発は初めてですか? 最初に、 作業の開始 に関するチュートリアルから始めます。 ここでは、クラス、メソッド、制御フローについて解説します。

別の言語で経験がありますか? このチュートリアルでは、ルールがオブジェクトの現在の状態に依存する場合に、C# パターンでオブジェクトの動作を明確に表現する方法について説明します。

このチュートリアルでは、運河ロックのルールをモデル化するコンソール アプリを構築します。

運河の水門は、水位が異なる 2 つの水域の間で、船を上下させる役割を果たします。 2 つのゲートと、水位が「低」と「高」の間で変化する水室を備えています。 この水門が安全に作動するには、水位とゲートの位置が適切な組み合わせの範囲内にある必要があります。

このチュートリアルでは、以下の内容を学習します。

  • 状態に応じてオブジェクトの動作を表現します。
  • C# パターン マッチングを使用してこれらの規則を実装します。
  • コンパイラ診断を使用して実装を検証してください。

Prerequisites

インストール手順

Windows では、この WinGet 構成ファイル を使用して、すべての前提条件をインストールします。 既に何かがインストールされている場合、WinGet はその手順をスキップします。

  1. ファイルをダウンロードし、ダブルクリックして実行します。
  2. 使用許諾契約書を読み、y と入力し、同意を求められたら Enter キーを選択します。
  3. タスク バーで点滅するユーザー アカウント制御 (UAC) プロンプトが表示された場合は、インストールを続行します。

他のプラットフォームでは、これらの各コンポーネントを個別にインストールする必要があります。

  1. 推奨されるインストーラーを .NET SDK ダウンロード ページからダウンロードし、ダブルクリックして実行します。 ダウンロード ページでプラットフォームが検出され、プラットフォームの最新のインストーラーが推奨されます。
  2. Visual Studio Codeホーム ページから最新のインストーラーをダウンロードし、ダブルクリックして実行します。 このページではプラットフォームも検出され、リンクはシステムに適している必要があります。
  3. C# DevKit 拡張機能ページの [インストール] ボタンをクリックします。 これで Visual Studio Code が開き、拡張機能をインストールするか有効にするかを確認するメッセージが表示されます。 [インストール] を選択します。

閘門のシミュレーションを作成する

閘門は水位の異なる水路間で船を上下させる。 このチュートリアルでは、シミュレーション対象の水門には下部ゲートと上部ゲートがあり、水位は低い状態か高い状態のいずれかになります。

通常の操作では、ロック内の水位が進入側のレベルと一致するとボートが入ります。 ボートが中に入ると、両方のゲートが閉じます。 水位が出口側に合わせて変化し、出口ゲートが開きます。 モデルの安全性を確保するため、水位は両方のゲートが閉じている場合にのみ変化することができ、ゲートはその側の水位と一致している場合にのみ開くことができます。

これらのルールは、 CanalLock クラスを使用してモデル化できます。 これは、いずれかのゲートを開いたり閉じたり、水を上げたり下げたりするコマンドを提供します。 また、ロックの現在の状態を示すプロパティも公開されます。

クラスを定義する

コンソール プロジェクトを作成し、 CanalLockという名前のクラスを追加します。 まず、パブリック API を設計し、メソッドを実装せずに残します。

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

上記のコードは、両方のゲートが閉じており、水位が低い状態でロックを初期化します。 次に、最初の実装の参考として、Main に以下のコードを追加してください。

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

次に、安全規則を適用せずに各状態値を変更する最初の実装を追加します。

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

これら最初のチェックは問題なく通過します。 仕組みはうまく機能しています。 次に、最初のエラー条件のテストを追加します。 前のシーケンスの最後に、両方のゲートが閉まっており、水位が低いです。 上のゲートを開いてみてください。

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

そのテストは失敗します。これは、上のゲートが開くべきではないときに開くからです。 最初の修正は次のようになります。

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

テストは再び合格します。 しかし、条件を追加すればするほど、if のステートメントが蓄積されます。 各ルールは他のルールから分離されているため、コードのスキャンが困難になります。

パターンを使用してコマンドを実装する

より明確なオプションは、 パターン を使用して有効な組み合わせを直接記述することです。 次の手順では、各 switch 式で パターン入力として 1 つのタプルが使用されます。 C# はそのタプルを 1 回評価し、各スイッチ アームは現在のゲート状態、水位、および要求された新しい設定をテストします。

上のゲートでは、次のような組み合わせをまとめることができます。

新しい設定 ゲートの状態 水位 Result
終了 終了 High 終了
終了 終了 低 終了
終了 Open High 終了
閉鎖 Open Low 閉鎖
Open 終了 High Open
Open 終了 低 閉 (エラー)
Open Open High Open
Open Open Low 閉 (エラー)

取り消し線が引かれた行は、無効な内部状態を表します。 switch 式は、有効な遷移を直接エンコードできます。 false は依然としてゲートが閉じていることを意味します:

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

このバージョンをお試しください。 テストは合格します。 また、は列挙型であり、C# では列挙型の基になる数値型の任意の値を、名前付きメンバーを持たない値であっても、その列挙型にキャストできるため、switch 式がWaterLevelではないこともコンパイラは警告します。 不可能な内部状態を処理するために、破棄パターン (_) を持つ最後のアームを追加します。

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

破棄パターンは残りの入力と一致するため、そのアームは最後である必要があります。

その後、前の分岐を簡略化できます。 ゲートを閉じる操作は常に許可されているため、1 つのアームで 4 つの個別の「閉」のケースを置き換えることができます:

(false, _, _) => false,

有効なオープン ケースを組み合わせて、その 1 件の安全エラーを保持することもできます。

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

プログラムをもう一度実行します。 テストに合格します。 最終的な 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"),
    };
}

残りのルールを実装する

次に、同じアイデアを SetLowGate と SetWaterLevelに適用します。 まず、無効な操作を明らかにするテストを追加します。

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

再度アプリを実行します。 これらのテストは失敗し、運河の水門は無効な状態になります。 完全な状態で照合することで、残りのメソッドを実装します。 SetLowGate は、SetHighGate と似ています。 SetWaterLevel は、現在の水位と両方のゲート位置を使用します。

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

考慮すべき組み合わせは 16 個です。 完全なテーブルから始めて、switch文の各分岐を記述し、テストを実行して、繰り返しの結果を簡略化します。

最終的に、このようなメソッドになりましたか?

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

これでテストは成功し、運河の水門は安全ルールを確実に適用されます。

Summary

このチュートリアルでは、パターン マッチングを使用して、オブジェクトがある有効な状態から別の有効な状態にどのように変化するかを表しました。 パターンは許可された遷移をまとめることで、長い一連の分岐ステートメントよりも簡単に比較できます。 この方法は、動作が複数の状態値の結合された形状に依存する場合に適しています。