你可能曾在程式碼審查中聽過「請在這個類別加上 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 就是將這項建議落實在程式碼中的工具。
「既然不是為繼承而設計,就不將它開放」這個意圖,會由編譯器強制執行。
意圖清楚後,日後閱讀程式碼的同事也不必再猜測。
那是不是所有類別都應該加上它?
這裡有件重要的事:在 Swift 中,參考型別不一定要是類別。
如果狀態可以用值來處理,struct 通常是更好的選擇。因為 struct 從一開始就沒有繼承。
也就是說,在煩惱「要不要加 final」之前,應該先問「這真的非得是類別不可嗎?」
如果仍然必須使用類別,就依照以下標準判斷。
| 情況 | 判斷 |
|---|---|
| 未考慮繼承的類別 | 加上 final(預設) |
| 效能敏感的重複呼叫程式碼 | 使用 final 引導靜態分派 |
| 設計了明確擴充點的框架 | 不加 final,保持開放 |
| 以繼承為前提的 API,例如 UIViewController | 不加 |
總結來說,記住以下幾點就很方便。
- 沒有繼承意圖時,預設加上 final
- 只開放真正想提供的擴充點
- 如果 struct 就能完成,從一開始就避免使用類別
面試時會這樣問
Q. 加上 final 關鍵字有什麼好處?
它能阻止繼承與覆寫,清楚表達設計意圖。同時,編譯器可以使用靜態分派取代動態分派,降低呼叫成本,也能進行內嵌最佳化。
Q. 那麼所有類別都應該加上 final 嗎?
若類別是為繼承而設計並撰寫文件,就應該保持開放。不過,沒有這種意圖的類別,預設關閉會更安全;如果值語意適合,也應優先考慮 struct。
final 這一行不只是單純的最佳化技巧,而是對「要如何使用這個類別」的回答。
從今天開始,每次建立新類別時,都問自己一次:「有必要開放繼承嗎?」只要養成這個習慣,程式碼就會穩固許多。

