多載與覆寫。這兩個概念的名稱相似到讓人懷疑,為什麼要取成這樣。
這是考試和面試的熱門題目,而容易混淆的主要原因就是名稱。兩者的運作原理完全不同:一個是建立多個同名函式,另一個則是重新定義繼承而來的函式。
兩者的決定時機也不同:一個在編譯期,另一個在執行期。這項差異也延伸到靜態派送與動態派送的效能議題。
本文會用 Swift 範例,清楚區分兩者。
以下是重點總結。
- 多載:定義多個同名但參數不同的函式。與繼承無關
- 覆寫:子類別重新定義父類別的方法。以繼承為前提
- 多載在編譯期決定呼叫哪個函式;覆寫則在執行期決定
- 覆寫是多型的核心機制
多載:同一個名稱定義多次
多載是指定義多個名稱相同、但參數組成(數量、型別、標籤)不同的函式。
func area(radius: Double) -> Double { .pi * radius * radius }
func area(width: Double, height: Double) -> Double { width * height }
func area(side: Double) -> Double { side * side }
編譯器會根據呼叫端傳入的引數判斷是哪個函式。寫下 area(radius: 3) 的瞬間,就確定使用第一個函式了。在執行前便已決定。
想想沒有多載的 C 語言,就能理解它有多方便。絕對值函式依型別使用不同名稱:abs、labs、fabs。多載讓相同概念可以使用相同名稱。
Swift 在標準函式庫各處都使用多載,例如 print(:) 和 min(:_:)。透過運算子多載,單一個 + 也能處理整數加法、字串串接和陣列合併。
有一點要注意:最好避免只有回傳型別不同的多載。Swift 在某些情況下允許這種語法,但如果呼叫端沒有明確指定型別,就會產生模稜兩可的錯誤,也讓讀者困惑。
覆寫:重新定義繼承而來的方法
覆寫是指子類別以相同的簽章,重新定義從父類別繼承的方法。前提是存在繼承關係。
class Animal {
func speak() { print("...") }
}
class Dog: Animal {
override func speak() { print("汪汪") }
}
class Cat: Animal {
override func speak() { print("喵") }
}
重點就在這個場景。
let animals: [Animal] = [Dog(), Cat()]
for animal in animals {
animal.speak() // 汪汪、喵
}
變數型別是 Animal,但實際執行的是實例的方法。要呼叫哪個 speak,會在執行期查看實際物件後決定。這就是動態派送,也是多型運作的機制。
Swift 強制使用 override 關鍵字也值得一提。編譯器會捕捉不小心建立與父類別方法相同簽章,或因拼字錯誤而覆寫失敗的情況。反過來,在父類別加上 final 就能禁止覆寫。
用表格一次比較
| 分類 | 多載 | 覆寫 |
|---|---|---|
| 定義 | 同名、不同參數 | 重新定義相同簽章 |
| 繼承 | 不需要 | 必要 |
| 決定時機 | 編譯期(靜態) | 執行期(動態) |
| 目的 | 名稱一致性、API 使用便利性 | 多型、替換行為 |
| Swift 關鍵字 | 無 | override(必要),可用 final 禁止 |
決定時機的差異也與效能有關。編譯期確定的呼叫(靜態派送)可以直接跳轉,但執行期決定的呼叫(動態派送)必須經過實際型別的方法表。在 Swift 中,加上 final 或 private 後,編譯器便有機會將動態派送改為靜態派送並進行最佳化。
不再混淆的記憶法
按照英文單字的原意記憶即可。
Overload = 超載。就是在同一個名稱上「載入」多個函式。一個名稱,多種功能。
Override = 忽略並踩過。就是「覆寫」父類別的定義。定義只有一個,但替換其內容。
面試時可以這樣開始回答。
「多載是放置多個參數不同的同名函式,在編譯期決定;覆寫是重新定義繼承而來的方法,在執行期根據實際型別決定。建立多型的是覆寫。」
總結
- 多載:定義多個同名、參數組成不同的函式。不需要繼承
- 覆寫:子類別以相同簽章重新定義父類別方法。必須繼承
- 多載在編譯期(靜態)決定呼叫目標,覆寫則在執行期(動態)決定
- 多型的核心機制是覆寫與動態派送
- Swift 透過強制使用 override 關鍵字防止錯誤,也能用 final 禁止覆寫
- 記憶法:Overload 在名稱上超載,Override 覆寫定義

