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.

推薦閱讀:

相關文章