軟體設計

為什麼 Swift 的 final 關鍵字應該加在類別上(效能與設計總整理)

你可能曾在程式碼審查中聽過「請在這個類別加上 final」這類說法。

閱讀 4 分鐘
為什麼 Swift 的 final 關鍵字應該加在類別上(效能與設計總整理) 封面圖

你可能曾在程式碼審查中聽過「請在這個類別加上 final」這類說法。

看似只是習慣,但了解原因後,你看待程式碼的方式會有所不同。

先說結論,final是在宣告「這個類別不打算透過繼承來擴充」。這短短一行同時兼顧效能與設計穩定性,影響比想像中更大。

今天就來整理為什麼 Swift 類別常被要求加上 final,以及背後真正的原因。


final 到底是什麼?

final 是阻止繼承與覆寫的關鍵字。

加在類別前面時,其他類別就無法繼承該類別。

加在方法或屬性前面時,也可以只禁止該成員被覆寫。

final class Logger {
    func log(_ msg: String) { print("[LOG] \(msg)") }
}

let logger = Logger()
logger.log("付款完成")   // 輸出: [LOG] 付款完成

如果為了繼承 Logger 而寫成 class FileLogger: Logger,就會發生編譯錯誤。

也就是把「這個類別到此為止」這件事釘死。


加上 final 後為什麼會變快?

第一個原因是效能。關鍵在於分派方式。

只要繼承仍然開放,編譯器就無法在執行階段之前知道實際會呼叫哪個方法,因此每次都必須查表尋找。

這稱為動態分派。它會先查詢每個類別的函式表(vtable),再進行呼叫。

相反地,加上 final 後,情況就不同了。

既然子類別無法介入,編譯器就能確定「這個呼叫一定會執行這個方法」。

因此不必查表,就能直接呼叫。這就是靜態分派。

再進一步,較短的方法甚至可以進行內嵌,把程式碼直接展開到呼叫位置。

單次呼叫的成本不高,但如果某個方法在迴圈中被呼叫數千次,差異就會累積。

一次呼叫會在這個分岔處分成靜態或動態
一次呼叫會在這個分岔處分成靜態或動態

真正的原因不是效能,而是「設計」

其實我認為這點比效能更重要。

那就是能防止脆弱基底類別問題(fragile base class)。

一旦開放繼承,任何人都能自由介入你所建立之父類別的內部行為。

明明只是無意間修改了父類別的一個方法,卻可能讓所有覆寫該方法的子類別接連損壞。

繼承是最容易破壞封裝的關係,因為子類別會看見父類別的內部細節。

因此物件導向設計原則也會這樣說:

為繼承而設計並撰寫文件;若不打算這麼做,就禁止繼承。—這是《Effective Java》的知名建議。

final 就是將這項建議落實在程式碼中的工具。

「既然不是為繼承而設計,就不將它開放」這個意圖,會由編譯器強制執行。

意圖清楚後,日後閱讀程式碼的同事也不必再猜測。

不打算開放就使用 final,讓編譯器守住意圖
不打算開放就使用 final,讓編譯器守住意圖

那是不是所有類別都應該加上它?

這裡有件重要的事:在 Swift 中,參考型別不一定要是類別。

如果狀態可以用值來處理,struct 通常是更好的選擇。因為 struct 從一開始就沒有繼承。

也就是說,在煩惱「要不要加 final」之前,應該先問「這真的非得是類別不可嗎?」

如果仍然必須使用類別,就依照以下標準判斷。

情況 判斷
未考慮繼承的類別 加上 final(預設)
效能敏感的重複呼叫程式碼 使用 final 引導靜態分派
設計了明確擴充點的框架 不加 final,保持開放
以繼承為前提的 API,例如 UIViewController 不加

總結來說,記住以下幾點就很方便。

  • 沒有繼承意圖時,預設加上 final
  • 只開放真正想提供的擴充點
  • 如果 struct 就能完成,從一開始就避免使用類別

面試時會這樣問

Q. 加上 final 關鍵字有什麼好處?

它能阻止繼承與覆寫,清楚表達設計意圖。同時,編譯器可以使用靜態分派取代動態分派,降低呼叫成本,也能進行內嵌最佳化。

Q. 那麼所有類別都應該加上 final 嗎?

若類別是為繼承而設計並撰寫文件,就應該保持開放。不過,沒有這種意圖的類別,預設關閉會更安全;如果值語意適合,也應優先考慮 struct。


final 這一行不只是單純的最佳化技巧,而是對「要如何使用這個類別」的回答。

從今天開始,每次建立新類別時,都問自己一次:「有必要開放繼承嗎?」只要養成這個習慣,程式碼就會穩固許多。

延伸閱讀