泛型的基本使用
通過泛型系統,可以減少很多冗餘代碼。
例如,不使用泛型時,定義一個參數允許為u8、i8、u16、i16、u32、i32......等類型的double函數時:
fn double_u8(i: u8) -> u8 { i + i } fn double_i8(i: i8) -> i8 { i + i } fn double_u16(i: u16) -> u16 { i + i } fn double_i16(i: i16) -> i16 { i + i } fn double_u32(i: u32) -> u32 { i + i } fn double_i32(i: i32) -> i32 { i + i } fn double_u64(i: u64) -> u64 { i + i } fn double_i64(i: i64) -> i64 { i + i } fn main(){ println!("{}",double_u8(3_u8)); println!("{}",double_i16(3_i16)); }
上面定義了一堆double函數,函數的邏輯部分是完全一致的,僅在於類型的不同。
泛型可以用於解決這樣因類型而代碼冗餘的問題。使用泛型時:
use std::ops::Add; fn double<T>(i: T) -> T where T: Add<Output=T> + Clone + Copy { i + i } fn main(){ println!("{}",double(3_i16)); println!("{}",double(3_i32)); }
上面的字母T就是泛型(和變量x的含義是相同的),它用來代表各種可能的數據類型。多數時候泛型使用單個大寫字母來表示,但也可以使用多個字母來表示。
對於double函數簽名的前一部分:
#![allow(unused)] fn main() { fn double<T>(i: T) -> T }
函數名稱後面的<T>
表示在函數作用域內定義一個泛型T,這個泛型只能在函數簽名和函數體內使用,就跟在一個作用域內定義一個變量,這個變量只能在該作用域內使用是一樣的。而且,泛型本就是代表各種數據類型的變量。
參數部分i: T
表示參數i的類型是泛型T。
返回值部分-> T
表示該函數的返回值類型是泛型T。
因此,上面這部分函數簽名表達的含義是:傳入某種數據類型的參數,也返回這種數據類型的返回值,且這種數據類型可以是任意的類型。
對於第一次接觸泛型的人來說,這可能很難理解。但是,換成類似的使用普通變量的代碼,可能就容易理解了:
# 偽代碼:傳入一個數據,返回這個數據
function f(x) {return x}
對泛型進行限制
但注意,double函數期待的是對數值進行加法操作,但泛型卻可以代表各種類型,因此,還需要對泛型T進行限制,否則在調用double函數時就允許傳遞字符串類型、Vec類型、Person類型等值作為函數參數,這偏離了期待。
例如,在double的函數體內需要對泛型T的值i進行加法操作,但只有實現了std::ops::Add
Trait的類型才能使用+
進行加法操作。因此要限制泛型T是那些實現了std::ops::Add
的數據類型。
限制泛型也叫做Trait綁定(Trait Bound),其語法有兩種:
- 在定義泛型類型T時,使用類似於
T: Trait_Name
這種語法進行限制 - 在返回值後面、大括號前面使用where關鍵字,如
where T: Trait_Name
因此,下面兩種寫法是等價的:
#![allow(unused)] fn main() { fn f<T: Clone + Copy>(i: T) -> T{} fn f<T>(i: T) -> T where T: Clone + Copy {} // 更復雜的示例: fn query<M: Mapper + Serialize, R: Reducer + Serialize>( data: &DataSet, map: M, reduce: R) -> Results { ... } // 此時,下面寫法更友好、可讀性更高 fn query<M, R>(data: &DataSet, map: M, reduce: R) -> Results where M: Mapper + Serialize, R: Reducer + Serialize { ... } }
其中,T: Trait_Name
表示將泛型T限制為那些實現了Trait_Name Trait的數據類型。因此T: std::ops::Add
表示泛型T只能代表那些實現了std::ops::Add
Trait的數據類型,比如各種數值類型都實現了Add Trait,因此T可以代表數值類型,而Vec類型沒有實現Add Trait,因此T不能代表Vec類型。
觀察指定變量數據類型的寫法i: i32
和限制泛型的寫法T: Trait_Name
,由此可知,Trait其實是泛型的數據類型,Trait限制了泛型所能代表的類型,正如數據類型限制了變量所能存放的數據。
有時候需要對泛型做多重限制,這時使用+
即可。例如T: Add<Output=T>+Copy+Clone
,表示限制泛型T只能代表那些同時實現了Add、Copy、Clone這三種Trait的數據類型。
之所以要做多重限制,是因為有時候限制少了,泛型所能代表的類型不夠精確或者缺失某種功能。比如,只限制泛型T是實現了std::ops::Add
Trait的類型還不夠,還要限制它實現了Copy Trait以便函數體內的參數i被轉移所有權時會自動進行Copy,但Copy Trait是Clone Trait的子Trait,即Copy依賴於Clone,因此限制泛型T實現Copy的同時,還要限制泛型T同時實現Clone Trait。
簡而言之,要對泛型做限制,一方面的原因是函數體內需要某種Trait提供的功能(比如函數體內要對i執行加法操作,需要的是std::ops::Add
的功能),另一方面的原因是要讓泛型T所能代表的數據類型足夠精確化(如果不做任何限制,泛型將能代表任意數據類型)。
泛型的引用類型
如果參數是一個引用,且又使用泛型,則需要使用泛型的引用&T
或&mut T
。
例如:
#![allow(unused)] fn main() { use std::fmt::Display; fn f<T: Display>(i: &T) { println!("{}", *i); } }
零運行開銷的泛型:泛型代碼膨脹
rustc在編譯代碼時,會將所有的泛型替換成它所代表的具體數據類型,就像編譯期間會將變量名替換成它所代表數據的內存地址一樣。
例如,對於下面這個泛型函數:
use std::ops::Add; fn double_me<T>(i: T) -> T where T: Add<Output=T> + Clone + Copy { i + i } fn main() { println!("{}", double_me(3u32)); println!("{}", double_me(3u8)); println!("{}", double_me(3i8)); }
在編譯期間,rustc會根據調用double_me()時傳遞的具體數據類型進行替換。上面示例使用了u32、u8和i8三種類型的值傳遞給泛型參數,那麼編譯期間,編譯器會對應生成三個double_me()函數,它們的參數類型分別是u32、u8和i8。
$ rustc src/main.rs
$ strings main | grep "double_me"
_ZN4main9double_me17h6d861a9e8ab36c42E
_ZN4main9double_me17ha214a9977249a1bfE
_ZN4main9double_me17hbc458c5fab68c203E
由於編譯期間,編譯器會對泛型類型進行替換,這會導致泛型代碼膨脹(code bloat),從一個函數膨脹為零個、一個或多個具體數據類型的函數。有時候這種膨脹會導致編譯後的程序文件變大很多。不過,多數情況下,代碼膨脹的問題都不是大問題。
另一方面,由於編譯期間已經將泛型替換成了具體的數據類型,因此,在程序運行期間,直接調用對應類型的函數即可,不需要再消耗任何額外的資源去計算泛型所代表的具體類型。因此,Rust的泛型是零運行時開銷的。