單型化(monomorphization)
/ MON-oh-MOR-fih-zay-shun /
當你寫一個泛型函式,像是 fn max<T: Ord>(a: T, b: T) -> T,你寫的是一段對許多型別都能運作的程式碼——i32、String、你自己的結構。但機器最終是在具體資料上跑具體指令;沒有單一一段機器碼能同時比較「兩個整數」和「兩個字串」,因為比較本身就不同。那麼一份泛型原始碼怎麼變成真正的程式碼?單型化(monomorphization)就是 Rust(與 C++ 模板)給出的答案:在編譯期,對於這個泛型實際被用到的每個具體型別,編譯器都產生一份各自、完全特化、把那個型別代入的程式碼副本。
把它想成編譯器在做蓋印副本。若你的程式呼叫 max(3, 4) 與 max("a", "b"),編譯器會在幕後產生兩個函式——一個 max_for_i32、一個 max_for_String——各自的比較都被解析到那個型別確切的實作,且不留任何泛型的痕跡。名字就說明了:「mono-morph」意為「單一形態」——每個輸出函式都有單一具體型別,從一份多型(「多形態」)的原始碼轉成數份單型的。因為每份副本都是具體的,它裡面的呼叫都是直接的(靜態分派),最佳化器能積極地行內與特化,所以一個泛型演算法對每個型別都跑得正好和手寫的一樣快。這正是人們稱 Rust 泛型為「零成本抽象」時的意思:抽象存在於原始碼,卻在二進位裡消失、被完全特化掉了。
為何重要:單型化是靜態分派之速度背後的引擎,但速度並非免費——它在程式碼大小與編譯時間上有真實而誠實的代價。你用來實例化一個泛型的每個相異型別,都產生機器碼的另一份副本,所以一個被許多型別使用的、大量泛型的函式庫,可能把二進位撐大、並明顯拖慢編譯(這是 Rust 與 C++ 編譯比你預期慢的著名原因之一)。對比動態分派的真正取捨正是這個:單型化給你快速、可行內、但重複的程式碼;dyn Trait 給你一份共用副本加一次間接。當二進位大小或編譯時間開始傷人,那就是該考慮拿一些單型化的泛型去換特徵物件的訊號。
fn largest<T: PartialOrd + Copy>(a: T, b: T) -> T { if a > b { a } else { b } } largest(3i32, 9i32); // 編譯器蓋印出 largest::<i32> largest(2.5f64, 1.0f64); // 以及「另一份」largest::<f64> // 二進位裡有兩份具體副本;每次呼叫都是直接且可行內。 // 用越多型別 -> 越多副本 -> 二進位越大、編譯越慢。
一份泛型原始碼變成每個用到之型別一份具體副本——快且可行內,但這些副本會累積成程式碼大小。
單型化正是泛型在執行期「零成本」、在建構期卻非免費的原因:每個型別實例化都複製一份機器碼,撐大二進位、拖慢編譯。那份程式碼大小的代價,正是對抗動態分派那單一共用副本的真正砝碼。