想一次搞懂 OOP 多型、從覆寫到協定,卻越看越混亂嗎?
多載、覆寫和協定看起來很像,很容易搞混。
先說結論:多型就是「相同名稱的呼叫,會依目標而有不同的行為」。覆寫透過繼承實作,而協定則透過規範實作。
這篇文章會一步步整理這三個概念如何彼此關聯。
多型到底是什麼?
多型(Polymorphism)在希臘文中代表「多種(poly)形式(morph)」。
簡單來說,即使傳送相同訊息,不同物件也會做出不同回應。
例如,同樣叫狗和貓「發出聲音」,狗會汪汪叫,貓會喵喵叫。
呼叫端只要呼叫소리내()即可。實際要產生什麼內容,由各個物件自行決定。
多型的核心就是「呼叫端不需要知道」。
因為不管收到什麼,都能用相同的程式碼處理。
這項特性讓程式碼更有彈性。即使加入新動物,也不需要修改呼叫端的程式碼。
多載和覆寫有什麼不同?
名稱相似,是最容易混淆的地方。
多載是指建立多個同名函式,只改變參數。
覆寫是指子類別重新定義並覆蓋父類別建立的函式。
光用文字不容易理解,直接看程式碼吧。
class Animal {
func sound() { print("...") }
}
class Dog: Animal {
override func sound() { print("汪汪") } // 覆寫
}
let a: Animal = Dog()
a.sound()
// 輸出:汪汪
有趣的是,雖然a的型別是Animal,卻會發出Dog的聲音。
這就是在執行階段查看實際物件後決定的多型。
多載則有些不同。要呼叫哪個函式會在編譯時決定。
func add(_ a: Int, _ b: Int) -> Int { a + b }
func add(_ a: String, _ b: String) -> String { a + b }
print(add(1, 2)) // 輸出: 3
print(add("甲", "乙")) // 輸出:甲乙
因此,多載也稱為靜態多型,覆寫則稱為動態多型。
為什麼需要協定?
只靠繼承也能實現多型,那為什麼還需要協定?
繼承代表「is-a」關係,因此父類別只能有一個。貓很難同時是動物又屬於另一個階層。
協定採用「只要遵守這份規範,任何型別都可以」的方式,因此自由得多。
protocol Soundable {
func sound()
}
struct Cat: Soundable {
func sound() { print("喵喵") }
}
struct Car: Soundable {
func sound() { print("叭叭") }
}
let things: [Soundable] = [Cat(), Car()]
things.forEach { $0.sound() }
// 輸出:喵喵
// 輸出:叭叭
貓和汽車沒有任何繼承關係,卻能放進同一個陣列並以相同方式處理。
不靠繼承關係也能取得多型,這就是協定的力量。
這也是 Swift 這類語言優先採用協定而非繼承的原因。
什麼時候用繼承,什麼時候用協定?
我把實務上的判斷標準整理成表格。
| 情況 | 判斷 |
|---|---|
| 想繼承共用程式碼 | 繼承(覆寫) |
| 沒有關係,但想保證相同的行為 | 協定 |
| 想同時賦予多種特性 | 協定(可同時採用多個) |
| 想讓值型別(struct)具備多型 | 協定 |
只要記住幾個簡單原則即可。
- 想大量重複使用父類別實作時,用繼承
- 想定義「能做什麼」的能力時,用協定
- 如果在意可擴充性,優先考慮協定
面試時會這樣問
Q. 請說明多載和覆寫的差異。
多載是在相同名稱下定義不同參數,於編譯時決定。覆寫則是子類別重新定義繼承而來的方法,於執行時查看實際物件後決定。因此前者稱為靜態多型,後者稱為動態多型。
Q. 使用協定取代繼承有什麼好處?
繼承只能有一個父類別,也不能用於值型別;協定則能同時採用多個,也能套用在 struct 上。即使是不相關的型別,也能用相同規範整合處理,有利於擴充。
多型一開始可能很抽象,但只要抓住「呼叫端不需要知道」這句話,其他內容就會迎刃而解。
把今天學到的覆寫和協定各用程式碼試一次,就會真正熟悉。加油!

