Swift 與 Objective-C

多載 vs 覆寫:用 Swift 分辨兩者

多載與覆寫。這兩個概念的名稱相似到讓人懷疑,為什麼要取成這樣。

閱讀 4 分鐘
多載 vs 覆寫:用 Swift 分辨兩者 封面圖

多載與覆寫。這兩個概念的名稱相似到讓人懷疑,為什麼要取成這樣。

這是考試和面試的熱門題目,而容易混淆的主要原因就是名稱。兩者的運作原理完全不同:一個是建立多個同名函式,另一個則是重新定義繼承而來的函式。

兩者的決定時機也不同:一個在編譯期,另一個在執行期。這項差異也延伸到靜態派送與動態派送的效能議題。

本文會用 Swift 範例,清楚區分兩者。

以下是重點總結。

  1. 多載:定義多個同名但參數不同的函式。與繼承無關
  2. 覆寫:子類別重新定義父類別的方法。以繼承為前提
  3. 多載在編譯期決定呼叫哪個函式;覆寫則在執行期決定
  4. 覆寫是多型的核心機制

多載:同一個名稱定義多次

多載是指定義多個名稱相同、但參數組成(數量、型別、標籤)不同的函式。

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 就能禁止覆寫。

即使變數型別是 Animal,執行的仍是實際實例的方法
即使變數型別是 Animal,執行的仍是實際實例的方法

用表格一次比較

分類 多載 覆寫
定義 同名、不同參數 重新定義相同簽章
繼承 不需要 必要
決定時機 編譯期(靜態) 執行期(動態)
目的 名稱一致性、API 使用便利性 多型、替換行為
Swift 關鍵字 override(必要),可用 final 禁止

決定時機的差異也與效能有關。編譯期確定的呼叫(靜態派送)可以直接跳轉,但執行期決定的呼叫(動態派送)必須經過實際型別的方法表。在 Swift 中,加上 final 或 private 後,編譯器便有機會將動態派送改為靜態派送並進行最佳化。

多載在編譯期決定,覆寫在執行期決定
多載在編譯期決定,覆寫在執行期決定

不再混淆的記憶法

按照英文單字的原意記憶即可。

Overload = 超載。就是在同一個名稱上「載入」多個函式。一個名稱,多種功能。

Override = 忽略並踩過。就是「覆寫」父類別的定義。定義只有一個,但替換其內容。

面試時可以這樣開始回答。

「多載是放置多個參數不同的同名函式,在編譯期決定;覆寫是重新定義繼承而來的方法,在執行期根據實際型別決定。建立多型的是覆寫。」


總結

  • 多載:定義多個同名、參數組成不同的函式。不需要繼承
  • 覆寫:子類別以相同簽章重新定義父類別方法。必須繼承
  • 多載在編譯期(靜態)決定呼叫目標,覆寫則在執行期(動態)決定
  • 多型的核心機制是覆寫與動態派送
  • Swift 透過強制使用 override 關鍵字防止錯誤,也能用 final 禁止覆寫
  • 記憶法:Overload 在名稱上超載,Override 覆寫定義

延伸閱讀