對於 C/Java 語系的語言,都有 switch 語法。switch 語法用於多分支是一個標準的用法,但這個分支語法的各分支之間存在穿透性,所以需要 break
來切斷邏輯,這也成為 switch 語法中最重要的一個替在缺陷來源。此外,由於 switch 語句中各 case 的程式碼是在同一個作用域中,也會對程式碼造成一些不便。
C# 8.0 引入了 switch 表示式。C# 的 switch 表示式有著非常豐富的語法元素,可以和模式匹配和解構等語法元素協同工作 —— 這些都不在這裡細說,但是對傳統的 switch 語句 進行了一些改進:
- 透過箭頭 (
=>
) 標記處理了 case 和語句之間的一對一關係,不需要 break,不再穿透; - 作為表示式,可以而且必須返回值;
也產生了新的約束:
- switch 表示式一定要詳盡(邏輯一定會走進某一個 case,可以透過棄元模式兜底),否則可能會在執行時引發異常。
在 C# 8.0 釋出的同年,Java 12 也釋出並引入了 switch 表示式預覽。Java 的 switch 表示式實現比較簡單,就是 switch 語句到 switch 表示式的直接轉換,僅支援等值匹配。直到 2023 年 3 月 Java 20 釋出,switch 表示式才開始支援模式匹配。相比之下,Kotlin 的 when 表示式走在了前面。
在這個問題上 JavaScript 似乎走在了後面,不過在語言提供 switch 表示式之前,我們可以嘗試自己造個輪子。
思路當然是參考策略模式。假設有一個列表,這個列表裡的每個元素都包含了兩個因素:第一個用於判斷是否命中,第二個是個函式,得到一個計算結果。然後寫一個迴圈遍歷列表的每個元素,一旦某個元素命中,就執行元素攜帶的函式獲得結果,中斷迴圈,返回結果。如果列表的最後一個元素必定命中,那麼這個列表就是“詳盡”的。
那麼這個 when
函式可能會這樣寫(switch
是關鍵字,所以使用 when
來作為函式名):
// JS
function when(value, ...cases) {
for (const { is, run } of cases) {
if (is(value)) {
return run(value);
}
}
throw new Error("非詳盡");
}
這裡我們假設每個情況 (case) 都含有 is
方法用於判斷是否命中,用 run
方法儲存命中後需要執行的操作。相應地,我們可以經典的“拿分算等級”來進行測試:
// JS
function calcGrade(score) {
return when(
score,
{ is: v => v >= 0 && v < 80, run: v => `不合格 (${v})` },
{ is: v => v >= 80 && v < 100, run: v => `合格 (${v})` },
{ is: v => v == 100, run: v => `滿分 (${v})` },
{ is: _ => true, run: v => `無效 (${v})` },
);
}
for (let i = 0; i < 50; i++) {
const v = 70 + ~~(Math.random() * 35);
console.log(calcGrade(v));
}
在 calcGrade
實現中 when
的 case 列表最後一項採用了“永真”斷言,所以走到這一項的時候一定會命中,從邏輯上來永遠不會觸發 Error。如果是非“詳盡”的情況列表,就有可能觸發 Error。
不過現在從測試程式碼中就發現了兩個問題:
is
斷言是採用函式的形式,不能簡單地直接按值匹配;- 從呼叫形式上來說,
score
和後面的 case 元素是同級的,形式上區分不明顯; - 每次都要寫
is
和run
,條件多了寫起來也煩。
繼續改進 ——
// JS
function when(value) {
// when 的引數先給 switch 的值
// 返回一個函式來處理分支匹配 ②
return function (...cases) {
for (const [is, run] of cases) {
// ^^^^^^^^^ 從物件改為元組(陣列)③
if (value === is || (typeof is == "function" && is(value))) {
// ^^^^^^ 精確判斷 ①
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 斷言函式判斷
return run(value);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 可指定行為(函式)
}
}
throw new Error("非詳盡");
};
}
function calcGrade(score) {
return when(score)(
// ^^^^^^^^^^^ 這裡返回的是匹配處理的函式
[v => v >= 0 && v < 80, v => `不合格 (${v})`],
[v => v >= 80 && v < 100, v => `合格 (${v})`],
[100, () => "滿分 (100)"],
// ^^^ 可以指定匹配的值
// ^^ 計算不需要引數,可以不宣告
[_ => true, v => `無效 (${v})`],
// ^^^^^^^^^ 兜底的永真斷言
);
}
為什麼兜底斷言必須使用一個函式呢?因為 true
值也有可能是對應一種預想的分支情況。由於這個 when
是透過語義來實現而不是透過語法來實現的,所以這裡沒辦法定義一個安全的兜底斷言語法,只有用斷言函式會相對安全。
至此為止我們已經基本實現了 switch 表示式 (when),把它升級成 TypeScript
// TypeScript
type CaseCondition<T> = T extends Function ? never : ((t: T) => boolean) | T
type Case<T, R> = [CaseCondition<T>, (t: T) => R];
function when<T>(value: T): <R>(...cases: Case<T, R>[]) => R {
return function<R>(...cases: Case<T, R>[]): R {
for (const [is, run] of cases) {
if (value === is || (typeof is == "function" && is(value))) {
return run(value);
}
}
throw new Error("非詳盡");
};
}
function calcGrade(score: number) {
return when(score)(
[v => v >= 0 && v < 80, v => `不合格 (${v})`],
[v => v >= 80 && v < 100, v => `合格 (${v})`],
[100, () => "滿分 (100)"],
[_ => true, v => `無效 (${v})`],
);
}
這段程式碼當然可以直接用,但是如果使用 npm 可能會更方便一點:
npm install @jamesfancy/when
// TypeScript
import { when } from "@jamesfancy/when";
function calcGrade(score: number) {
return when(score)(
[v => v >= 0 && v < 80, v => `不合格 (${v})`],
[v => v >= 80 && v < 100, v => `合格 (${v})`],
[100, () => "滿分 (100)"],
[_ => true, v => `無效 (${v})`],
);
}