switch 陳述

December 19, 2021

在〈if 陳述〉中有個成績比對的範例,因為 Toy Lang 的 if...else 規定必須是右上 { 左下 },而且沒有提供 if...elif...else 這類的語法,因此會形成明顯的巢狀。

Toy 語法

實際上,該範例對成績區分為 A ~ D 與不及格,也就是列舉了五個可能的情況,基本上有關於列舉的匹配,就可以使用 switch 來處理:

score = Number.parseInt(input('輸入分數:'))
switch Number.parseInt(score / 10) {
    case 10, 9
        println('得 A')
    case 8
        println('得 B')
    case 7
        println('得 C')
    case 6
        println('得 D')
    default
        println('不及格')
}

A ~ D 與不及格怎麼列舉呢?主要就是根據分數的十位數字而定,因此這個範例適用於 switch 來列舉,switch 右邊運算出來的值,會與 case 的值逐一比對,若符合就執行 case 對應的陳述。

如你所見,Toy Lang 的 case 可以同時列舉多個值,值與值之間使用逗號區隔,可用於比對數值、字串,如果沒有任何值比對成功,就會執行 default 部份的陳述。

Toy 實作

如果你已經看過〈if 陳述〉,大概可以想像出 switch 是如何實作出來的,沒錯,switch 基本上也是個陳述句的容器,差別在於 switch 存放的陳述會有多個區段,基本上會像是:

class Switch {
    constructor(switchValue, cases, defaultStmt) {
        this.switchValue = switchValue;
        this.cases = cases; 
        this.defaultStmt = defaultStmt;
    }

    evaluate(context) {
        const value = this.switchValue.evaluate(context);
        return compareCases(context, value, this.cases) || this.defaultStmt.evaluate(context);
    }   
}

function compareCases(context, switchValue, cases) {
    if(cases.length === 0) {
        return false;  // no matched case
    }
    const cazeValues = cases[0][0];
    const cazeStmt = cases[0][1];
    return compareCaseValues(context, switchValue, cazeValues, cazeStmt) || 
            compareCases(context, switchValue, cases.slice(1));
}

function compareCaseValues(context, switchValue, cazeValues, cazeStmt) {
    if(cazeValues.length === 0) {
        return false; // no matched value
    }
    const v = cazeValues[0].evaluate(context);
    if(v.value === switchValue.value) {
        return cazeStmt.evaluate(context);
    }
    return compareCaseValues(context, switchValue, cazeValues.slice(1), cazeStmt);
}

要比對的 case 會有多個,而每個 case 會有一個以上的列舉值以及對應的陳述,這是 cases 裏儲存的,cases[n] 代表著第 ncasecases[n][0] 是列舉值清單,cases[n][1] 是陳述句清單,呃!用巢狀的清單好像不是很好,應該要定義個專用類別會比較清楚…這件事就交給你了…XD

簡單來說,取得 switch 要被比對的值之後,逐一與 case 中的列舉值比對,如果成功就執行對應的陳述,否則就是執行 default 裏的陳述,就跟語法功能上描述的是一致的。

倒是可以稍微與 if 對照一下,在一些自由格式的 C-like 語言中,可以寫 if...else if...else,因而形成瀑布式的流程,這樣的流程在外觀上,又有點像是 switch 可以做到的事,也就偶而有人會搞不清楚,什麼時候該用 if...else if...else,什麼時候該用 switch

有時就會這麼建議,使用 switch 的話,值只會在一開始運算一次,然後逐一與 case 的列舉值比對,不用重複運算出值來;如果使用 if...else if...else 的話,那麼在每個 if,條件式中的值都會運算一次,因此,若是 switchif...else if...else 都能解決的情況,使用 switch 會比較有效率。

就上面的 Switch 節點來看,確實值只會取出一次,之後就是 case 列舉值的比對,而在〈if 陳述〉中也看過 If 節點:

class If {
    constructor(cond, trueStmt, falseStmt) {
        this.cond = cond;
        this.trueStmt = trueStmt;
        this.falseStmt = falseStmt;
    }

    evaluate(context) {
        if(this.cond.evaluate(context).value) {
            return this.trueStmt.evaluate(context);
        }
        return this.falseStmt.evaluate(context);
    }   
}

每次 if 處,都會建立一個 If 節點,而每次 evaluate,就會對 cond 也執行一次 evaluate,若是 switchif...else if...else 都能解決的情況,使用 switch 會比較有效率,這種說法基本上是沒會的。

不過,若要就設計上而言, switchif...else if...else 的差別,在於 switch 是用在一些存在可列舉的情境,也就是值可以分類為特定幾個類型或狀態的情況,而 if...else if...else,老實說,是用在狀況二分的情況,一個狀況是某條件成立,一個狀況是某條件不成立,而不是多分支,多分支只是程式碼排版上造成的錯覺。

如〈if 陳述〉中看到的,在被限制為不能使用 if...else if...else 時,也就是無法形成瀑布式流程時,就能突顯出實際上沒有多分支,仍舊是 if...else 罷了,不過奇怪的,大家對巢狀很敏感,然而對瀑布式卻沒那麼敏感,甚至經常視而不見,幾十條 if...else if...else,明明就不正常,然而卻經常看到…XD