Protocol Oriented: Static Dispatch
Swift Protocol 簡介
Protocol 即協議。
相比繼承,Protocol 是一種非常優雅的擴展一個類的方式。類似 Java 中的 Interface,和某些語言的 Trait。
類型一旦遵守了某個協議,就必須實現該協議所列出的方法;如果對一個協議進行擴展,它也能提供這些方法的默認實現。
同時,Protocol 還能提供類型約束,我除了可以判斷某實例是不是某種類型,也可以判斷它是否遵從某個協議。
聲明
protocol Eatable {
/// Energy per 100 gram, in kCal.
var energy: Int { get }
}
extension Eatable {
var energy: Int {
return 0
}
func printDescription() {
print("每 100 克含熱量 (energy)kCal.")
}
}
class Apple: Eatable {
var energy: Int {
return 52
}
func printDescription() {
print("蘋果 ??,是總部位於美國加州庫比蒂諾的跨國科技公司。每 100 克含熱量 (energy)kCal.")
}
}
class Cheese: Eatable {
var energy: Int {
return 403
}
func printDescription() {
print("乳酪 ???,音譯芝士、起司、起士,是多種乳制乳酪的通稱,有各式各樣的味道、口感和形式。每 100 克含熱量 (energy)kCal.")
}
}
這個例子和另一篇文章中的有所不同,在於此處的 energy 有默認值 0。
協議「可食用」規定了成員變數 energy,其默認值為 0。同時提供了一個現成的方法 printDescription
,可以方便地列印出與熱量相關的用戶友好信息。
而 Apple 和 Cheese 覆寫了 printDescription
方法,輸出了與其本身關係更緊密的信息。
調用 1
協議和類型的聲明如上,之後我們來實例化和調用一下。
let apple = Apple()
apple.printDescription()
let cheese = Cheese()
cheese.printDescription()
// ---- 結果 ----
//
// 蘋果 ??,是總部位於美國加州庫比蒂諾的跨國科技公司。每 100 克含熱量 52kCal.
// 乳酪 ???,音譯芝士、起司、起士,是多種乳制乳酪的通稱,有各式各樣的味道、口感和形式。每 100 克含熱量 403kCal.
OK,符合預期。
調用 2
我們再封裝一個方法,此時使用 Eatable 來約束類型:
func learnAboutFood(_ food: Eatable) {
food.printDescription()
}
這樣的話,我們就可以把任意遵從 Eatable 的實例都扔進去:
let apple = Apple()
let cheese = Cheese()
learnAboutFood(apple)
learnAboutFood(cheese)
// ---- 結果 ----
//
// 每 100 克含熱量 52kCal.
// 每 100 克含熱量 403kCal.
OK...等一下?為什麼方法調用的是 printDescription()
的協議的默認實現?我的 Apple 和 Cheese 類應該覆寫過這個方法才對啊。
而且為什麼 printDescription()
調了默認實現,print 出來的 energy
的值卻不是默認提供的 0 呢?
如果是 Class 繼承的話...
如果這是在繼承的世界裡,事情完全不是這樣的:
class BaseClass {
func printDescription() {
print("Base")
}
}
class DerivedClass: BaseClass {
override func printDescription() {
print("Derived")
}
}
var object: BaseClass
object = BaseClass()
print("When object is BaseClass: ")
object.printDescription()
print("When object is DerivedClass: ")
object = DerivedClass()
object.printDescription()
// ---- 結果 ----
//
// When object is BaseClass:
// Base
// When object is DerivedClass:
// Derived
從輸出中可知,即便 object 的類型顯式聲明為 BaseClass,當它是一個 DerivedClass 實例的時候,它的方法依然指向 DerivedClass 中的實現。
靜態調用
其實原因很簡單。
由繼承而得到的多態表現,本質上是把所有的方法的實現都放在一張 virtual table 裏,每次調用都必須先在表中查詢,可以稱之為「動態調用」。因此纔有了「覆寫」的概念:方法被覆寫,即當子類調用該方法時,會使用子類的實現而非父類的。
而協議與類有所不同。協議所定義的方法,也和類一樣,是「動態調用」的;但那些未出現在定義中,而僅出現於 Extension 中的方法是「靜態調用」的,即這個方法屬於協議,不屬於協議的遵從者。
那麼為什麼 Apple 和 Cheese 中的 printDescription()
可以覆寫,也可以正確地表現出多態呢:
class Apple: Eatable {
var energy: Int {
return 52
}
func printDescription() {
print("蘋果 ??,是總部位於美國加州庫比蒂諾的跨國科技公司。每 100 克含熱量 (energy)kCal.")
}
}
let apple = Apple()
apple.printDescription()
// ---- 結果 ----
//
// 蘋果 ??,是總部位於美國加州庫比蒂諾的跨國科技公司。每 100 克含熱量 52kCal.
首先 Apple
中的 printDescription()
和 Eatable
的 Extension 中的 printDescription()
,已經是兩個無關的方法了,不存在覆寫一說。
因此 Apple 的實例調用這個方法,也只不過是調用了兩個同名的方法中的就近的那個而已。
靜態 -> 動態
實際上,靜態在性能上是優於動態的。
將不必要的動態方法靜態化,是非常好的編程習慣,也是性能優化的要點之一。
但本例中我們需要的是動態,其實也很簡單,把 printDescription()
也放進定義中即可:
protocol Eatable {
/// Energy per 100 gram, in kCal.
var energy: Int { get }
func printDescription()
}
extension Eatable {
var energy: Int {
return 0
}
func printDescription() {
print("每 100 克含熱量 (energy)kCal.")
}
}
調用結果:
func learnAboutFood(_ food: Eatable) {
food.printDescription()
}
let apple = Apple()
let cheese = Cheese()
apple.printDescription()
cheese.printDescription()
learnAboutFood(apple)
learnAboutFood(cheese)
// ---- 結果 ----
// 蘋果 ??,是總部位於美國加州庫比蒂諾的跨國科技公司。每 100 克含熱量 52kCal.
// 乳酪 ???,音譯芝士、起司、起士,是多種乳制乳酪的通稱,有各式各樣的味道、口感和形式。每 100 克含熱量 403kCal.
// 蘋果 ??,是總部位於美國加州庫比蒂諾的跨國科技公司。每 100 克含熱量 52kCal.
// 乳酪 ???,音譯芝士、起司、起士,是多種乳制乳酪的通稱,有各式各樣的味道、口感和形式。每 100 克含熱量 403kCal.
推薦閱讀: