泛型(Generics)
從前面的「所有權(Ownership)」章節開始,接著生命週期(Lifetime)、特徵(Trait)、列舉(Enum),對平常只有撰寫前端或是只有寫 CRUD 的工程師來說(例如我就是),應該開始有一點學習上爬坡感了,這個章節會再繼續的增加一點坡度。
假設我定義了一個長方型的 Struct:
struct Rectangle {
width: u32,
height: u32,
}
裡面有 width
跟 height
這兩個欄位,都是 u32
型別。Rust 對型別的要求是很嚴格的,說好的 u32
就是只能放整數,給它小數的話馬上 Rust 編譯器就會跳出來抱怨了。
let rect_a = Rectangle{ width: 100, height: 50 }; // 沒問題
let rect_b = Rectangle{ width: 38.5, height: 19.5 }; // 不行!
假設我希望這個 Rectangle
的欄位也可以使用小數的話,你可能想說可以改成寫成這樣:
struct RectangleU32 {
width: u32,
height: u32,
}
struct RectangleF32 {
width: f32,
height: f32,
}
這樣就沒問題了:
let rect_a = RectangleI32{ width: 100, height: 50 };
let rect_b = RectangleF32{ width: 38.5, height: 19.5 };
但如果這樣寫,後續如果想要寫一個可以計算面積的函數,這兩個不同的 Struct 要怎麼傳進去當參數?難道也要寫兩份?
不同的程式語言在面臨這樣的情境,都有類似的設計,有些程式語言稱 之「模板(Template)」,有些則稱之「泛型(Generics)」,就字面上的解釋來說泛型並不是特定指某一種型別,而是用來「代表」某個型別。如果還是覺得有點抽象,我們來看看程式碼:
struct Rectangle<T> {
width: T,
height: T,
}
首先,在 Struct 名字的最後面的 <T>
就是 Rust 裡泛型的標記寫法,表示在這個 Struct 裡會有一種 T
型別。
蛤?什麼是 T
型別?這你不用管,反正它就是一種型別,或說它就是個佔位置的 placeholder 也行,它可以是整數、浮點數、布林值或其它型別都可以,沒有限定。以上面的範例來說,唯一確定的就是 width
跟 height
這兩個欄位是「相同的 T
型別」。正是因為它可以廣「泛」的代表某種「型」別,才稱之「泛型」。
如果你喜歡的話,你可以把這個 T
換成任意的大寫開頭的字,像這樣:
struct Rectangle<RRRRRR> {
width: RRRRRR,
height: RRRRRR,
}
Rust 編譯器不會抱怨,但通常會看到大家使用 T
是因為它比較短而且可以用來代表「型別(Type)」,甚至在其它有支援泛型的程式語言也都會用 T
。把原本固定寫死的型別抽換成 T
型別之後,使用起來就更有彈性了:
let rect_a = Rectangle{ width: 100, height: 50 };
let rect_b = Rectangle{ width: 38.5, height: 19.5 };
現在 Rectangle
的欄位可以用整數,也可以用小數了。這樣的泛型寫法可以讓原本 Struct 更有彈性了,不需要一樣的東西寫很多次。
有趣的是,雖然這裡並沒有指定特定型別,但當 Rust 在編譯過程中發現你要 u32
型別,那它就在背後幫你做一個 u32
的 Struct 出來,你帶 f32
給它,它就幫你做一個欄位是 f32
的 Struct 給你,這個行為有個專有名詞叫「單型化 (Monomorphization)」:
struct Rectangle_i32 {
width: i32,
height: i32,
}
struct Rectangle_f32 {
width: f32,
height: f32,
}
實際產生的名字應該更複雜,但這些都是在編譯時期 Rust 編譯器幫你做掉的,所以雖然在編譯的階段會多花一點點的時間,但執行的時候不會有額外的成本,效能不會因為這樣而有所折扣,也就是 Rust 官方手冊裡所提到「零成本抽象(Zero-Cost Abstractions)」的概念,你不用手動寫出這些資料結構的實際型別。
多個不同型別
承上,如果 width
跟 height
這兩個欄位我想要給它們不同的型別呢?沒問題,你可以這樣寫:
struct Rectangle<T, U> {
width: T,
height: U,
}
那個 U
就是表示另一種型別。同樣的,你想用什麼名字都行,不過慣例上會使用 U
,因為它正好是英文字母 T
的下一個字,如果還有更多型別參數,可以繼續往下使用 V
、W
之類的字母。(就跟為什麼迴圈裡常會用 i
變數,然後迴圈裡的迴圈會用 j
變數一樣的道理)
泛型不是只能用在 Struct 上,Enum 跟 Trait 也都能用。現在各位再回想一下在上個章節介章提到的 Option
跟 Result
:
pub enum Option<T> {
None,
Some(T),
}
pub enum Result<T, E> {
Ok(T),
Err(E),
}