從原理分析Swift的switch怎麼比較物件

bestswifter發表於2017-12-27

今天突然想到一個問題,讓我覺得有必要總結一下switch語句。我們知道swift中的switch,遠比C語言只能比較整數強大得多,但問題來了,哪些型別可以放到switch中比較呢,物件可以比較麼?

官方文件對switch的用法給出了這樣的解釋:

Cases can match many different patterns, including interval matches, tuples, and casts to a specific type.

也就是說除了最常用的比較整數、字串等等之外,switch還可以用來匹配範圍、元組,轉化成某個特定型別等等。但文件裡這個including用的實在是無語,因為它沒有指明所有可以放在switch中比較的型別,文章開頭提出的問題依然沒有答案。

我們不妨動手試一下,用switch匹配物件:

class A {

}

var o  = A()
var o1 = A()
var o2 = A()

switch o {
case o1:
print("it is o1")
case o2:
print("it is o2")
default:
print("not o1 or o2")
}
複製程式碼

果然,編譯器報錯了:“Expression pattern of type 'A' cannot match values of type 'A'”。至少我們目前還不明白“expression pattern”是什麼,怎麼型別A就不能匹配型別A了。

我們做一下改動,在case語句後面加上let

switch o {
case let o1:
print("it is o1")
case let o2:
print("it is o2")
default:
print("not o1 or o2")
}
複製程式碼

OK,編譯執行,結果是:it is o1。這是因為case let不是匹配值,而是值繫結,也就是把o的值賦給臨時變數o1,這在o是可選型別時很有用,類似於if let那樣的隱式解析可選型別。沒有打出it is o2是因為swift中的switch,只匹配第一個相符的case,然後就結束了,即使不寫break也不會跳到後面的case。

扯遠了,回到話題上來,既然新增let不行,我們得想別的辦法。這時候不妨考慮一下switch語句是怎麼實現的。據我個人猜測,估計類似於用了好多個if判斷有沒有匹配的case,那既然如此,我們給型別A過載一下==運算子試試:

class A {}

func == (lhs: A, rhs: A) -> Bool { return true }

var o = A(); var o1 = A() ;var o2 = A()

switch o {
case o1:
print("it is o1")
case o2:
print("it is o2")
default:
print("not o1 or o2")
}
複製程式碼

很顯然,又失敗了。如果這就能搞定問題,那這篇文章也太水了。報錯資訊和之前一樣。可問題是我們已經過載了==運算子,為什麼A型別還是不能餓匹配A型別呢,難道switch不用判斷兩個變數是否相等麼。

switch作為一個多條件匹配的語句,自然是要判斷變數是否相等的,不過它不是通過==運算子判斷,而是通過~=運算子。再來看一段官方文件的解釋:

An expression pattern represents the value of an expression. Expression patterns appear only in switch statement case labels.

以及這句話:

The expression represented by the expression pattern is compared with the value of an input expression using the Swift standard library ~= operator.

第一句解釋了之前的報錯,所謂的“express pattern”是指表示式的值,這個概念只在switch的case標籤中有。所以之前的報錯資訊是說:“o1這個表示式的值(還是o1)與傳入的引數o都是型別A的,但它們無法匹配”。至於為什麼不能匹配,答案在第二句話中,因為o1和o的匹配是通過呼叫標準庫中的~=運算子完成的。

所以,只要把過載==換成過載~=就可以了。改動一個字元,別的都不用改,然後程式就可以執行了。Swift預設在~=運算子中呼叫==運算子,這也就是為什麼我們感覺不到匹配整數型別需要什麼額外處理。但對於自定義型別來說,不過載~=運算子,就算你過載了==也是沒用的。

除此以外,還有一種解決方法,那就是讓A型別實現Equatable協議。這樣就不需要過載~=運算子了。答案就在Swift的module的最後幾行:

@warn_unused_result
public func ~=<T : Equatable>(a: T, b: T) -> Bool
複製程式碼

Swift已經為所有實現了Equatable協議的類過載了~=運算子。雖然實現Equatable協議只要求過載==運算子,但如果你不顯式的註明遵守了Equatable協議,swift是無法知道的。因此,如果你過載了==運算子,就順手標註一下實現了Equatable協議吧,這樣還有很多好處,比如SequenceTypesplit方法等。

最後總結一句:

能放在switch語句中的型別必須過載~=運算子,或者實現Equatable協議。

相關文章