共用方式為


使用模式比對來建置您的類別行為,以提升程序代碼

C# 中的模式比對功能提供表達演演算法的語法。 您可以使用這些技術在類別中實作行為。 您可以將面向物件類別設計與數據導向實作結合,以在模型化真實世界物件時提供簡潔的程序代碼。

在本教學課程中,您將瞭解如何:

  • 使用數據模式表達您的面向物件類別。
  • 使用 C# 的模式比對功能來實作這些模式。
  • 利用編譯程式診斷來驗證您的實作。

先決條件

您必須設定電腦以執行 .NET。 下載 Visual Studio 2022.NET SDK

建置運河鎖的模擬

在本教學課程中,您會建立一個模擬 運河船閘的 C# 類別。 簡言之,運河鎖是一種裝置,在不同水位的兩段水之間移動時,會提高和降低船隻。 鎖有兩個閘門,還有一些機制可以用來改變水位。

在正常作業中,一艘船進入其中一個閘門,而鎖中的水位與船進入的一側的水位相符。 一旦進入鎖,水位就會變更為符合船離開鎖的水位。 一旦水位符合該側,出口端的大門就會開啟。 安全措施確保操作員無法在運河中造成危險情況。 只有在兩個閘門關閉時,水位才能改變。 最多一個門可以打開。 若要開啟閘門,船閘中的水位必須與閘門外的水位相符。

您可以建置 C# 類別來建立此行為的模型。 CanalLock 類別支援命令來開啟或關閉任一閘道。 它會有其他命令來提高或降低水。 類別也應該支援屬性,以讀取閘門和水位的目前狀態。 您的方法會執行安全措施。

定義類別

您可以建置主控台應用程式來測試 CanalLock 類別。 使用 Visual Studio 或 .NET CLI 建立適用於 .NET 5 的新控制台專案。 然後,新增類別並將它命名為 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}");

接下來,在 CanalLock 類別中新增每個方法的第一個實作。 下列程式代碼會實作 類別的方法,而不必擔心安全規則。 您稍後會新增安全測試:

// 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 子句,並測試不同的屬性。 很快,當您新增更多條件時,這些方法會變得太複雜。

使用模式實作命令

更好的方法是使用 模式 來判斷物件是否處於有效的狀態來執行命令。 作為三個變數(閘道的狀態、水位,以及新的設定)的函數,您可以表達一個命令是否被允許。

新增設定 閘道狀態 水位 結果
關閉 已關閉 關閉
關閉 關閉 已關閉
關閉 打開 關閉
關閉 開啟 關閉
打開 關閉 打開
打開 關閉 關閉 (錯誤)
打開 打開 打開
開啟 開啟 關閉 (錯誤)

表格中的第四行和最後一行的文字被劃去,因為它們無效。 您現在要新增的程式代碼應該確保水位偏低時,不會開啟高水閘。 這些狀態可以編碼為單一道開關表達式(請記住,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
};

請嘗試此版本。 您的測試通過了,驗證了程式碼。 完整數據表會顯示輸入和結果的可能組合。 這表示您和其他開發人員可以快速查看該表,並了解您已涵蓋所有可能的輸入。 更簡單的是,編譯器也能提供幫助。 新增先前的程式代碼之後,您可以看到編譯程式會產生警告訊息:CS8524 表示 switch 表達式未涵蓋所有可能的輸入。 該警告的原因是其中一個輸入是 enum 類型。 編譯程式會將「所有可能的輸入」解譯為基礎類型的所有輸入,通常是 int。 這個 switch 表示式只會檢查 enum中所宣告的值。 若要移除警告,您可以為表達式的最後一個分支新增通用捨棄模式。 此條件會擲回例外狀況,因為它表示輸入無效:

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

前一個切換臂必須在您的 switch 表示式中列為最後,因為它符合所有輸入。 依照順序稍早移動它來進行實驗。 這會導致編譯器錯誤 CS8510,因為模式中有無法達到的程式碼。 switch 表達式的自然結構可讓編譯程式針對可能的錯誤產生錯誤和警告。 編譯器的「安全網」讓您更輕鬆地在較少的反覆中撰寫正確的程式碼,並享有將 switch 區塊與通配符結合的自由。 如果您的組合導致出乎意料的分支無法到達,編譯器會發出錯誤;而如果您移除了必要的分支,則會給出警告。

第一個變更是將所有關閉大門的部件結合起來;這樣一直是允許的。 將下列程式代碼新增為 switch 運算式中的第一個 arm:

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

再次執行測試並通過。 以下是 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"),
    };
}

自行實作模式

既然您已瞭解這項技術,請自行填入 SetLowGateSetWaterLevel 方法。 首先,加入以下程式碼以測試這些方法的無效操作:

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

再次執行您的應用程式。 您可以看到新的測試失敗,運河鎖進入無效的狀態。 嘗試自行實作其餘的方法。 設定下閘道的方法應該類似於設定上層閘道的方法。 變更水位的方法有不同的檢查,但應該遵循類似的結構。 為了用來設定水位的方法,您可能會發現使用相同的流程會有所幫助。 從這四個輸入開始:兩個閘口的狀態、水位的目前狀態,以及所要求的新水位。 switch 表達式的開頭應為:

CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
{
    // elided
};

你總共有16個開關臂需要填入。 然後,測試並簡化。

您是否開發了類似這樣的方法?

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

您的測試應該通過,運河鎖應該安全地運作。

總結

在本教學課程中,您已瞭解如何使用模式比對來檢查對象的內部狀態,然後再將任何變更套用至該狀態。 您可以檢查屬性的組合。 建置任何轉換的數據表之後,即可測試程式代碼,然後簡化可讀性和可維護性。 這些初始重構可能會建議進一步重構,以驗證內部狀態或管理其他 API 變更。 本教學課程將類別和對象與實作這些類別的數據導向模式型方法結合在一起。