深入 Rust

靜態對比動態分派(static vs dynamic dispatch)

當你的程式碼透過一個特徵呼叫一個方法時,有個根本問題:編譯器在建構時就知道究竟哪個函式會跑,還是程式必須在執行時才弄清楚?「分派(dispatch)」就只是把一個呼叫解析到某個具體函式的說法。靜態分派在編譯期決定;動態分派在執行期決定。Rust 讓你選擇,而這個選擇正是寫 fn f<T: Trait>(x: T)(泛型、靜態)與寫 fn f(x: &dyn Trait)(特徵物件、動態)之間的差別。

用靜態分派時,你使用帶特徵界限的泛型。對於你實際用來呼叫該函式的每個具體型別,編譯器都印出一份各自特化的函式副本,其中的方法呼叫被解析到確切的實作——這就是單型化(monomorphisation)。所得的呼叫是對一個已知函式的直接呼叫,最佳化器能將它行內展開,於是一個泛型的「依 Ord 排序」在編譯後會變成像你親手為那一個型別寫的一樣精煉的程式碼。用動態分派時,你使用特徵物件(dyn Trait)。函式只有一份副本,而每次呼叫都從值所攜帶的 vtable 讀出函式指標並透過它呼叫——一次無法行內展開的間接呼叫,代價是多一次記憶體載入加上一個較難預測的分支。誠實的總結:靜態分派每次呼叫較快且可行內,但每個型別產生一份程式碼副本(程式碼膨脹);動態分派是一份共用副本且有彈性(一個異質的 Vec<Box<dyn Trait>>),但每次呼叫都付一次間接的代價。

為何重要:這是 Rust 裡最實際的效能選擇之一,而正確答案通常很無聊。對於速度要緊、型別已知、且型別集合很小的熱點程式碼,預設用泛型(靜態分派);當你真的需要在一個集合裡混合型別、當單型化造成的編譯時間或二進位大小開始傷人、或當間接的代價相對於方法所做的工作微不足道時,就採用 dyn Trait(動態分派)。常見誤解是以為動態分派「慢」到處處都要緊——通常那次間接呼叫淹沒在雜訊裡,而 dyn 的可讀性或彈性才是更好的交換。在假設代價要緊之前,先量測。

trait Speak { fn say(&self) -> &str; } // 「靜態」分派:每個具體 T 一份特化副本,直接且可行內。 fn shout<T: Speak>(x: &T) { println!("{}!", x.say()); } // 「動態」分派:一份副本,每次呼叫都走 vtable。 fn shout_dyn(x: &dyn Speak) { println!("{}!", x.say()); } // 只有 dyn 讓你把混合型別存在一起: // let crowd: Vec<Box<dyn Speak>> = vec![ /* 不同型別 */ ];

泛型靜態分派(快、可行內、每型別一份程式碼);dyn Trait 動態分派(一份副本,每次使用一次間接呼叫)。

動態分派並非「處處都慢」。那次間接呼叫相對於方法的實際工作通常微不足道,而 dyn 的彈性往往更划算。對於熱點、型別已知的路徑預設用泛型;在你必須混合型別、或單型化把二進位撐大時採用 dyn——並且量測。

又稱
dispatchmonomorphic vs virtual callimpl Trait vs dyn Trait靜態分派動態分派