你可能聽過「在類別加上 final 就會變快」。這聽起來像都市傳說,但背後其實有精確的機制。
本文接續上一篇文章 Swift 進階 #4。
看似理所當然的方法呼叫,實際上會以三種不同方式編譯。採用哪一種,會影響效能與最佳化可能性。
進階系列本篇談的是分派。
呼叫的三張臉——直呼叫、表格與訊息
像 object.doSomething() 這樣的程式碼,翻譯成機器碼時有三種方式。
靜態分派。 編譯時就能確定函式,因此直接跳到函式位址呼叫。
它最快;更重要的是,編譯器能執行內嵌,移除呼叫並嵌入主體,進而開啟後續最佳化。struct 方法、全域函式與 final 方法都屬於此類。
表格分派。 支援繼承與覆寫的類別方法會採用這種方式。
即使變數的編譯期型別是 Animal,實際執行個體也可能是 Dog。要呼叫哪個實作,必須到執行期才知道。
因此每個類別都有虛擬函式表(vtable)。呼叫時會從執行個體的表格找出函式索引並跳轉。多型性的代價是一次間接參照。
協定也有稱為 witness table 的近親,作用相同。這就是 some 與 any 那一篇提到的表格。
訊息分派。 這是 Objective-C 執行環境的方式,透過 objc_msgSend 進行以名稱為基礎的查找。
三者中它最慢,但換來極高的彈性,甚至能在執行期替換方法。在 Swift 中,加上 @objc dynamic 的方法就會進入這個世界。
objc_msgSend 的內部已在另一篇文章介紹,這裡先不展開。
歸根究柢是彈性與速度的交換。越依賴執行期決定就越慢;越能在編譯期固定,就越快,也越能最佳化。
final 與 private 為何是效能關鍵字
現在可以拆解這個都市傳說了。編譯器在什麼條件下能透過去虛擬化,將表格分派靜態降級?
必須證明「這個方法不可能被覆寫」。
final 正是這項證明。final class 與 final func 宣告「子類別不會重新定義」。
編譯器可以移除 vtable 查找,改成直接呼叫,也同時開啟內嵌的大門(Swift Optimization Tips)。
private 也有類似效果。檔案外不可見的宣告,讓編譯器能看見檔案內所有使用處,進而證明沒有覆寫。
啟用整個模組最佳化(WMO,現今發行組建的預設值)後,證明範圍會擴大到整個模組。沒有子類別的 internal 類別也會自動視為 final。
實務規則可以整理成一句話:沒有繼承規劃的類別就加上 final。
這不只是設計宣告(此型別是繼承樹的葉節點),對編譯器而言也是一份最佳化證明。
但別期待「加了 final,App 就變快」。呼叫開銷以奈秒計,除非位於熱迴圈中,否則幾乎無感。
真正有價值的是內嵌後連鎖發生的最佳化,而那些由編譯器自動處理。我們只需避免阻擋這項證明。
協定 extension 的陷阱——它是需求嗎?
分派知識在實務上最能發揮作用的地方(更準確地說,是不懂時最容易踩雷的地方)就是協定預設實作。先看一道小測驗。
protocol Greeter {
func hello() // 宣告為需求
}
extension Greeter {
func hello() { print("你好") }
func bye() { print("再見") } // 不是需求
}
struct Korean: Greeter {
func hello() { print("嗨") }
func bye() { print("拜拜") }
}
let k: any Greeter = Korean()
k.hello() // ?
k.bye() // ?
答案是「你好」與「再見」。hello 是協定需求,因此會透過 witness table 動態分派,Korean 的實作也已註冊到表格中。
相對地,bye 是不在需求清單中的 extension 專用方法。透過協定型別呼叫時,會靜態直接呼叫 extension 實作,不論 Korean 定義了什麼。
不懂這條規則,就會變成「明明實作了,卻沒有呼叫我的程式碼」的神祕錯誤。解法很簡單。
凡是採用者必須能替換的方法,都要在協定本體宣告為需求。extension 的預設實作可以保留,但本體宣告才會在表格中建立位置。
POP(協定導向程式設計)篇曾將協定 extension 介紹為繼承的替代方案;這就是使用該工具的安全規則。
透過量測確認——不要靠感覺,要用分析工具
分派話題的結尾總該是同一個警告:這屬於微最佳化領域,而順序很重要。
先用 Instruments 的 Time Profiler 找出真正的瓶頸。大多數效能問題不是分派,而是演算法(O(n²) 迴圈)、不必要的工作(每個畫面都重新計算)或 I/O。
反對過早最佳化的提醒,就如同 Knuth 那篇文章所說。
如果分析真的抓到熱迴圈內的動態分派,這時才列出處方:讓型別成為 final、將 any 換成 some 或泛型以促成特化,或把協定邊界移到迴圈外。
反過來說,這項知識平常的用途不是最佳化,而是理解設計。
為什麼 struct 是預設值(適合靜態分派)、為什麼建議使用 some 而非 any(可進行特化),以及為什麼 SwiftUI 使用 struct view。
語言的重要決策全都扎根於這一層;了解分派後,就能把 Swift 的設計讀成一幅完整圖像。
總結
- 方法呼叫會編譯為靜態(直接)、表格(vtable/witness table)或訊息(objc_msgSend)分派;彈性與速度成反比。
- final、private 與 WMO 能證明「沒有覆寫」,將動態呼叫降為靜態呼叫並開啟內嵌。不打算繼承的類別,final 就是基本功。
- 協定 extension 方法是否動態分派,取決於是否宣告為需求。必須可替換的方法,一定要宣告在協定本體中。
- 順序是先用分析工具。分派知識平常的價值不是最佳化技巧,而是讀懂語言設計。
下一篇再往下一層:探討記憶體配置,包括 struct 大小如何決定、屬性順序為何會改變記憶體,以及 any 箱子的實際大小。
延伸閱讀
來源與驗證
- Swift Optimization TipsSwift 프로젝트 · 官方文件 · 查核 2026年8月17日依據: 靜態與動態分派、final、whole-module optimization 與效能特性

![[Swift 進階 #5] Swift 分派三兄弟:final 為何能提升效能 封面圖](/assets/images/posts/f974f02c-833c-4ab1-9350-a2e3543e8391/swift-method-dispatch-1.jpg)