[C#] 延伸學習 Switch Expression 的使用方式

延伸學習 Switch Expression 的使用方式

前言

筆者先前文章【使用 Switch Expression 建立 State Machine 控管審核流程】有使用到 C# 8.0 的 Switch Express 特性來建立一個簡單的 state machine 機制,這篇文章再針對 Switch Express 的應用繼續學習。

 

 

Switch Expression 

以先前文章中的操作來看,主要都是定義好條件,當符合條件時「直接」回傳一個值出來。

public class StateMachine
{
    /// <summary>
    /// 狀態機目前狀態
    /// </summary>
    private FormState _currentState;

    /// <summary>
    /// 建構子
    /// </summary>
    /// <param name="state">初始狀態</param>
    public StateMachine(FormState state)
    {
        _currentState = state;
    }

    /// <summary>
    /// 定義動作與狀態互動關係
    /// </summary>
    /// <param name="action">動作</param>
    /// <returns>新狀態</returns>
    public FormState Manipulate(FormAction action)
        => (_currentState, action) switch
        {
            // 【草稿】 執行"送審" => 【審核中】
            (FormState.Draft, FormAction.Submit) => FormState.Approving,

            // 【待審核】 執行"核准" => 【審核通過】
            (FormState.Approving, FormAction.Approve) => FormState.Approved,

            // 【待審核】 執行"退回" => 【送審被拒】
            (FormState.Approving, FormAction.Reject) => FormState.Reject,

            // 【審核通過】 執行"編輯" => 【草稿】
            (FormState.Approved, FormAction.Edit) => FormState.Draft,

            // 【送審被拒】 執行"編輯" => 【草稿】
            (FormState.Reject, FormAction.Edit) => FormState.Draft,

            _ => throw new Exception($"目前狀態 {_currentState} 不允許執行 {action} 動作!"),
        };
}

 

 

有邏輯操作的空間嗎?

我們是不是可以像在傳統的 switch case 中插入一段邏輯於其中,決定要 return 的資料是什麼? 答案是可以,如下定義一個 GetState 方法,並傳入 Func 實際要取得新狀態的邏輯方法即可;在實際的使用情境上,也許不單只有 _currentState 及 action 就可以決定下個新 state 狀態,也許會有一些特定「外部條件」來影響並決定這組 _currentState  及 action 對於狀態變化,因此就可以利用這種方式來處理。

/// <summary>
/// 取得狀態
/// </summary>
/// <param name="f">取得狀態的方法</param>
/// <returns>狀態</returns>
private FormState GetState(Func<FormState> f) => f();

/// <summary>
/// 定義動作與狀態互動關係
/// </summary>
/// <param name="action">動作</param>
/// <returns>新狀態</returns>
public FormState Manipulate(FormAction action)
    => (_currentState, action) switch
    {
        // 【草稿】 執行"送審" =>
        (FormState.Draft, FormAction.Submit) => GetState(() =>
        {
            // 判斷系統參數來決定是否已啟用審核機制
            var isApproveFlowEnabled = IsUseApproveFlow();

            // (已啟用審核機制) ? [待審核]  : [審核通過]
            return isApproveFlowEnabled ? FormState.Approving : FormState.Approved;
        }),

        // 【待審核】 執行"核准" => 【審核通過】
        (FormState.Approving, FormAction.Approve) => FormState.Approved,

        // 【待審核】 執行"退回" => 【送審被拒】
        (FormState.Approving, FormAction.Reject) => FormState.Reject,

        // 【審核通過】 執行"編輯" => 【草稿】
        (FormState.Approved, FormAction.Edit) => FormState.Draft,

        // 【送審被拒】 執行"編輯" => 【草稿】
        (FormState.Reject, FormAction.Edit) => FormState.Draft,

        _ => throw new Exception($"目前狀態 {_currentState} 不允許執行 {action} 動作!"),
    };

 

 

有多餘的條件怎樣處理

若以前述方式來處理邏輯,會比較難以一目了然的看出個別情境的條件邏輯,因此可考慮將「外部條件」邏輯整理成簡單的 flag 傳入,使用 when 來加註狀態,讓列表更清晰表達出 switch case 條件式。

public FormState ManipulateYo2(FormAction action)
{
    var isApproveFlowEnabled = IsUseApproveFlow();
    
    return (_state, action) switch
    {
        // [草稿] 送審後 => [待審核] (已啟用審核機制)
        (FormState.Draft, FormAction.Submit) when isApproveFlowEnabled => FormState.Approving,

        // [草稿] 送審後 => [審核通過] (未啟用審核機制)
        (FormState.Draft, FormAction.Submit) when !isApproveFlowEnabled => FormState.Approved,

        // [待審核] 審後 => [審核通過]
        (FormState.Approving, FormAction.Approve) => FormState.Approved,

        // [審核通過] 編輯 => [草稿]
        (FormState.Approved, FormAction.Edit) => FormState.Draft,

        // [待審核] 退回 => [送審被拒]
        (FormState.Approving, FormAction.Reject) => FormState.Reject,

        // [送審被拒] 編輯 => [草稿]
        (FormState.Reject, FormAction.Edit) => FormState.Draft,

        _ => throw new BusinessException(BusinessError.FormFlowInvalid,
            _errorCodeLocalizer["BusinessError.FormFlowInvalid", _state, action]),
    };
}

 

但這樣有個缺點,每個陳述無法被檢核為獨一無二的條件,例如以下相同條件無法被 IDE 偵測出來,當在複雜情境下使用時有些不便,會有重複定義的問題產生。

 

所以我會傾向多定義一個參數,若部分的 switch case 沒使用到這個參數時,可以使用 _ 忽略它。

public FormState ManipulateYo1(FormAction action)
{
    var isRequiredApprove = IsUseApproveFlow();

    return (_state, action, isRequiredApprove) switch
    {
        // [草稿] 送審後 => [待審核] (已啟用審核機制)
        (FormState.Draft, FormAction.Submit, true) => FormState.Approving,

        // [草稿] 送審後 => [審核通過] (未啟用審核機制)
        (FormState.Draft, FormAction.Submit, false) => FormState.Approved,

        // [待審核] 審後 => [審核通過]
        (FormState.Approving, FormAction.Approve, _) => FormState.Approved,

        // [審核通過] 編輯 => [草稿]
        (FormState.Approved, FormAction.Edit, _) => FormState.Draft,

        // [待審核] 退回 => [送審被拒]
        (FormState.Approving, FormAction.Reject, _) => FormState.Reject,

        // [送審被拒] 編輯 => [草稿]
        (FormState.Reject, FormAction.Edit, _) => FormState.Draft,

        _ => throw new BusinessException(BusinessError.FormFlowInvalid,
            _errorCodeLocalizer["BusinessError.FormFlowInvalid", _state, action]),
    };
}

 

這樣如果有相同條件出現,就可以馬上被檢核出來,避免重複定義的問題產生。

 

 

參考資訊

C# 8 Switch Expressions with Pattern Matching

 


希望此篇文章可以幫助到需要的人

若內容有誤或有其他建議請不吝留言給筆者喔 !