利用SwitchExpression取代if-else,並閃避逐行判斷的寫法
認知中的C# Switch Expression有分成兩種運作模式,一種是執行效能較高的傳統switch寫法,命中了就回傳離開,另外一種是效能較低逐行掃描型態的寫法,但無論是哪一種寫法都可以精簡程式碼的數量,在這邊透過整理陳列供大家參考。
至於是高效能單一命中就離開還是逐行掃描,我認為應該是語法糖背後的編譯器,默默地幫忙COVER掉所有的轉換工作啦(我猜,未證實)。
傳統高效能:執行完成就離開
下方是一個非常傳統的SwitchExpression寫法,透過單步執行我們可以發現,每次迴圈進入到i switch
後,只會命中一步就離開,例如當j=1
回傳1
後,就會直接離開往下一次迴圈繼續執行,運作模式跟傳統的switch case ...
完全一致。
for (int i = 0; i < 10; i++)
{
var j = i switch
{
1 => 1,
2 => 2,
_ => 3,
};
}
再改成下方這種邏輯判斷的寫法(不要在乎邏輯,這邊純粹是要測試語法與執行方式):
for (int i = 0; i < 10; i++)
{
var j = i switch
{
>= 1 and < 2 => 1,
>= 2 and < 3 => 2,
_ => 3,
};
}
這樣的寫法在VisualStudio單步追蹤可以發現,依然是每次進入i switch
後,只會命中一步就離開,也就是說除了1回傳1
、2回傳2
之外,其他的都回傳0且毫不囉嗦的離開,運作模式與傳統的switch case ...
認知一致,但你可以發現傳統的switch case ...
其實並沒有辦法進行太多的邏輯判斷(其實C# 7.0後的switch case when ...
還是可以辦到),這就是SwitchExpression寫法逐漸勝出之處。
逐行低效能:每一個判段都執行才離開
然而世界並非如此簡單,任何事情都可以用SwitchExpression提供的基本判斷式子就可以解決,下面我們先創造一個產生DataTable
假資料的方法,從這個方法中,我們也可以瞥見SwitchExpression已經開始引入when
的條件式寫法了。
static System.Data.DataTable MakeTable()
{
var oDT = new System.Data.DataTable();
oDT.Columns.AddRange(
"cID, iMoney, dDate"
.Split(',')
.Select(x => {
return x.Trim() switch {
string y when y.StartsWith("i") => new System.Data.DataColumn(y, typeof(System.Int32)),
string y when y.StartsWith("d") => new System.Data.DataColumn(y, typeof(System.DateTime)),
_ => new System.Data.DataColumn(x.Trim(), typeof(System.String))
};
})
.ToArray()
);
oDT.Rows.Add("A123456787,9999,2023-10-26".Split(",".ToCharArray()));
oDT.Rows.Add("A123456788,8888,2023-10-27".Split(",".ToCharArray()));
oDT.Rows.Add("A123456789,7777,2023-10-28".Split(",".ToCharArray()));
return oDT;
}
接著我們創造雙迴圈去讀取這個DataTable裡面的值,並逐一取出來判斷是否有問題,這時候我們的SwitchExpression就要改寫成這樣:
var oDT = MakeTable();
foreach (System.Data.DataRow oRow in oDT.Rows)
{
for (int i = 0; i < oDT.Columns.Count; i++)
{
var j = i switch {
int x when (x == 0 && oRow[x].ToString().Length != 10) => throw new System.Exception($"身分證號必須為10碼。"),
int x when (x == 1 && !System.Int32.TryParse(oRow[x].ToString(), out int y)) => throw new System.Exception($"金額必須為正整數。"),
int x when (x == 2 && !System.DateTime.TryParseExact(oRow[x].ToString(), new string[] { "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss" }, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out System.DateTime y)) => throw new System.Exception($"日期格式必須正確。"),
_ => ""
};
}
}
從上面的程式碼我們可以發現,他會逐一地把DataRow的每一個欄位值拿出來檢查,但本文要討論的是這樣的寫法之下,SwitchExpression的運作模式就再也不是命中>執行>跳到下一個迴圈,而是真的會逐筆比對。
舉例來說,明明已經陷入i=0
的條件去判斷文字的長度是否為10碼,但執行判斷完成後,他還是會接續陷入的下一個條件,也就是是否為正整數
的判斷,然而這個判斷明明就是i=1
才需要關心的事情。所以,每一個條件都進行一次評估的作法,已經回到If-Else
的工作模式了,如果應用的判斷式一多,例如資料表高達30個欄位,且資料筆數超多,那麼耗用的執行時間就會呈現等比級數的增長了。
⚠ 另外這邊要特別特別特別
注意的地方是,因為我們MakeTable()
給的資料都是正確的資料,也就是根本不會有拋出System.Exception
的機會,因此i=0 ~ 2
每一條都評估沒有命中後,一定都會跑去執行default條件一次:_ => ""
。
如果喜歡更精簡的寫法,可以再把上面的程式碼更簡化成下列:
var oDT = MakeTable();
foreach (System.Data.DataRow oRow in oDT.Rows)
{
for (int i = 0; i < oDT.Columns.Count; i++)
{
var j = i switch
{
_ when (i == 0 && oRow[i].ToString().Length != 10) => throw new System.Exception($"身分證號必須為10碼。"),
_ when (i == 1 && !System.Int32.TryParse(oRow[i].ToString(), out _)) => throw new System.Exception($"金額必須為正整數。"),
_ when (i == 2 && !System.DateTime.TryParseExact(oRow[i].ToString(), new string[] { "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss" }, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out _)) => throw new System.Exception($"日期格式必須正確。"),
_ => ""
};
}
}
從上面新的寫法可以發現,我們把原本宣告的暫時性int x
變數移除改用_
符號來取代,此外進行TryParse
後動態宣告的外拋變數,因為沒有使用到,因此也被用_
符號來取代,程式碼變得更精簡了。但是,這樣的寫法仍然會落入逐一掃瞄判斷的程序之中。
把逐一掃瞄判斷的改回執行完就離開
天無絕人之路,SwitchEpression的寫法有無窮的可能性。下面展示如何把上面這種逐一掃瞄、每一行都判斷執行的寫法,改回只有判斷一次,運行過就離開的寫法,這樣可以大幅度的提升程式執行的效能喔。
var oDT = MakeTable();
foreach (System.Data.DataRow oRow in oDT.Rows)
{
for (int i = 0; i < oDT.Columns.Count; i++)
{
_ = i switch
{
0 when oRow[i].ToString().Length != 10 => throw new System.Exception($"身分證號必須為10碼。"),
1 when !int.TryParse(oRow[i].ToString(), out _) => throw new System.Exception($"金額必須為正整數。"),
2 when !DateTime.TryParseExact(oRow[i].ToString(), new[] { "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss" }, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out _) => throw new System.Exception($"日期格式必須正確。"),
_ => ""
};
}
}
從上面的程式碼我們可以發現,回到給定特定的欄位值
搭配when
,強制SwitchExpression回到做完後就離開的模式,在這樣的寫法下,舉例以i=1
的情況下,他跑完1 when ...
這一行後就會離開了。另外也可以發現其實我們根本不需要先前被拿來宣告的var j
變數,因此這邊一併使用_
符號來取代掉。
⚠ 另外這邊還是要特別特別特別
再叮嚀一次,因為我們MakeTable()
給的資料都是正確的資料,也就是根本不會有拋出System.Exception
的機會,因此例如i=0
跑評估後沒有命中,雖然再也不會去跑i=1
或i=2
的判斷式,但還是會跳到default去跑一次:_ => ""
。反之,如果有命中某個判斷式(例如身分證號長度不等於10),這時候就會直接出Exception
離開了。
相關連結
- 在C#的SwitchExpression下使用模式比對(Pattern Matching)
- 利用SwitchExpression來進行switch流程程式碼判斷的優化
- 利用SwitchExpression取代if-else,並閃避逐行判斷的寫法
- 利用C#的switch case when語法來忽略字串大小寫
- C#的IS與AS運算子之撰寫方法
- C#的NULL運算演化:?.、??、??=、以及模式比對增強功能
- 遞迴模式比對
- C# 9.0 中的新增功能