這是和大部分的語言差不多的一個概念,只不過實現上有一些區別而已。
所以,如果學過java,c#,c++,那麼這應該很好理解。
雖然如此,還是有不少內容需要記錄,只不過內容有一點小多。
注意:這是入門級別內容,只能涉及到一些基本的方面。
一、定義
英文 Generic /generics, 中文翻譯為通用型別/泛型, 和java等語言是差不多的一個概念。
注意:實際指的是物件(這裡引用了java等oop的概念)的屬性和方法可以接收通用型別,不是指物件本身是通用的。只不過
透過對屬性和方法的通用化,實現某種程度的物件通用。
通用型別的引入產生了巨大的方便
在麼有通用型別的情況下,一個帶有一個引數的函式/方法只能適用於一種型別,如果類似的函式有多個,那麼必然照成了一定的工作量。
所以,通用型別的存在就是為了讓一個物件(屬性和其中方法)、方法可以適用於多種型別。 具體實現方式見後文。
如果您學過其它語言,這個就非常容易理解了。
二、實現通用型別
和java類似,rust可以在物件(struct,enum等)和方法中使用通用型別符號。
本文的例子基本來源於相關書籍。
以下那麼的例子,其實歸結起來就是兩個:方法中使用通用型別、變數/屬性中使用通用型別。
2.1 函式中使用通用型別
特別注意
rust自己把其它語言中的函式、方法做了區分
- 函式(function) - 不屬於結構、列舉的功能體
fn this_is_function()->i32{ 10; } fn main(){ this_is_function(); }
大體上可以這麼理解。
- 方法(method)-屬於結構、媒體的功能體
#[derive(Debug)] struct Cube{ length: u32, width: u32, height: u32, } impl Cube{ fn volume(&self) -> u32{ return self.length * self.width * self.height; } fn is_bigger_than(&self, other: &Cube) -> bool{ return self.volume() > other.volume(); } }
雖然二者的語法一致,包括關鍵字等,但是核心的區別是:函式不屬於某個物件(結構、列舉等),只會屬於某個模組。
這個例子,對於從來沒有接觸過通用型別的工程師而言,非常有用,因為它說清楚了使用了通用型別的函式和一般函式的區別。
這是典型的:口水說幹了,不如一個例子
例_2.1
fn largest_i32(list: &[i32]) -> &i32 { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } fn largest_char(list: &[char]) -> &char { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } //使用上通用型別後的取最大值函式,這裡只要一個即可 //如果沒有對T進行限定,會報告異常: binary operation `>` cannot be applied to type `&T` //所以必須使用 T: xxxx的方式進行限定 fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } fn main() { let number_list = vec![34, 50, 25, 100, 65]; let char_list = vec!['y', 'm', 'a', 'q']; let result = largest_i32(&number_list); println!("最大數值 of {:?} is {result}", number_list); let result = largest_char(&char_list); println!("最大字元 of {:?} is {result}", char_list); println!("使用帶通用型別引數的函式來計算最大值"); let result = largest(&number_list); println!("最大數值 of {:?} is {result}", number_list); let result = largest(&char_list); println!("最大字元 of {:?} is {result}", char_list); }
在上例中,函式largest利用通用型別T實現了整數取最大、字元取最大的功能。
如果有返回值,且返回型別也是通用型別,則必須在方法名後帶上<T>之類的,這一點和java等類似
2.2 結構體中使用通用型別
道理同2.1,稍微列舉下。
#[derive(Debug)] struct Family<數字,字元>{ qty:數字, name:字元 } fn main() { let family = Family{ qty: 10, name: "A".to_string(), }; println!("{:#?}", family); let mf= Family{ qty:"10個", name:"爸爸,媽媽,兒子,女兒" }; println!("{:#?}", mf); }
如果有結構方法,且 impl後的結構有帶上通用型別符號,則必須在impl後直接帶上<A,B,C...>之類的,同結構體自身一樣
impl<數字,String> Family<數字,String> { fn double(&self){ &self.qty; } }
也就是說如果Family有限定型別,則impl後必須帶上一樣的<T,U...>
如果Family不用通用型別符號,則impl也不必帶
impl Family<i32,String> { fn get(&self){ &self.qty; } }
2.3 列舉中使用通用型別
道理同2.1,稍微列舉下。
#[derive(Debug)] enum Position<A,B,C>{ 普通成員(A), 組長(B), 經理(C), 總監(C), 老闆(A,B,C) } fn main() { //如果你只使用部分通用型別符號,那麼必須使用":xxx"語法來列明其它通用符號的型別 //這種情況應該是使用於其它情況下 let p:Position<i32, i32, i32> = Position::普通成員(10); //如果直接定義為 let p=Position::普通成員(10);會報錯 type annotations needed for `Position<i32, _, _>` println!("{:?}",p); let boss=Position::老闆("lml",2045,[10,20,30,80,90,95,99,100]); println!("{:?}",boss); }
這裡需要注意是:如果例項變數只是使用部分通用型別符號,那麼必須使用":xxx"語法來列明其它通用符號的型別。
否則會報告類似這樣的錯誤: type annotations needed for `Position<i32, _, _>`
這是因為rust必須在編譯期間確定具體型別。
列舉定義方法,如果不用通用型別,如下,否則同結構。
impl Position<i32, i32, i32> { fn get_position(&self)->i32{ match self { Position::普通成員(a) => *a, Position::組長(a) => *a, Position::經理(a) => *a, Position::總監(a) => *a, _ => 100 } } }
2.4 其它型別中使用通用型別
除了廣泛使用的結構、列舉型別,rust的其它型別也可以使用通用型別。
只不過,這些型別通常需要作為結構列舉等複雜型別(物件)的屬性成員,才可以使用這些通用型別符號。
struct Box<A,B,C>{ names:(A,B,C), scores:HashMap<B,C>, days:[C;5], tools:Vec<A> }
它們自己單獨定義是沒有的,例如不能定義 let name:Vec<T>=Vec<i32>::new();
三、限定型別範圍
在例子2.1中,函式largest的,必須定義為:
四、在執行時判斷通用型別的實際型別
rust好像不支援這個。
rust是靜態型別語言,這意味著它在編譯的時候,就需要確定變數例項的型別。
而這也意味著,它不怎麼需要在執行時候:動態判定一個變數例項的具體的型別。
如果它那麼做,可能會違法它的核心理念:安全之上。
說到這個問題,不得不提到java。
java雖然是名義上的靜態型別語言,但是由於java的特殊性(一切都是類),它有辦法透過為物件設定屬性來判斷一個例項的型別。
java可以利用instaneof來判斷。
rust沒有這種機制,至少學到這裡是沒有的!
然而rust也有變通的方式改變這個,此處不論!
五、編譯與效能
這個比較有意思。
在入門級的書籍中,我們就能瞭解到這個事實:
- 泛型並不會使程式比具體型別執行得慢
- Rust 透過在編譯時進行泛型程式碼的 單態化(monomorphization)來保證效率。單態化是一個透過填充編譯時使用的具體型別,將通用程式碼轉換為特定程式碼的過程
monomorphization這是rust人制造的一個詞彙,大體由mono(單聲道)+morph(變化)+zation(化,使得...成立)構成,表面意思是:單聲道變化
只有結合例子才知道,其實就是中文"具體化"的意思:針對適配的具體型別,逐個編譯,使得每一種情況都被考慮到。
這種過程很類似設計模式中工廠模式-對外公佈一個介面,對內則實現各種工廠。
還是結合例子(來自原書籍),再看例_2.1:
fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest }
在上例中,如果只有一個通用型別函式,而沒有具體的,那麼,在編譯的時候,會根據相關環境,編譯出若干個函式出來(這裡是兩個):
fn largest_i32(list: &[i32]) -> &i32 { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } fn largest_char(list: &[char]) -> &char { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest }
當然,大體是這麼一個意思。這也是我自己的猜測。
書本上的例子更加直接,是列舉的。
enum Option_i32 { Some(i32), None, } enum Option_f64 { Some(f64), None, } fn main() { let integer = Option_i32::Some(5); let float = Option_f64::Some(5.0); }
意思就是,根據main的情況,自動反編譯出Option的兩個具體。
考慮到這種,需求,所以,不難理解為什麼rust採用靜態實現,為什麼不支援動態判斷變數例項的型別(待確認)。
六、小結
- 通用型別針對的是物件屬性,方法/函式引數,不是物件、方法本身。這個基本同java等語言
- 可以使用合法的識別符號來表示通用型別,不必一定是K,T,V之類的,包括單詞,漢語詞語等。這個方面和JAVA等語言類似
- rust並沒有對方法/函式一或者物件可以定義的通用型別進行個數限制
- 如果要對函式返回型別使用通用型別,則必須在函式名後跟上諸如<T>這樣的字元,這個方式和java的類似。當方法不需要(注意函式和方法的區別)
- rust一樣存在通用型別的常見問題:要麼定義的時候限定通用型別的型別,要麼是執行時在方法/函式體中使用類似 instanceof(java)之類的判斷泛型的實際型別。rust只能在編碼的時候進行限制
- 但rust可以使用類似 T:xxxx的方式限定型別範圍,或者T:xxxx+****+???+..,,其中xxxx,****,???都是表示特定型別/操作
- 截止本章為止,rust並沒有?這樣的萬用字元,可以用於通用型別中。java有類似 ? super/extends Grade這樣的用法,可用於限定引數型別
- rust透過在編譯階段的行為(單態化/具體化)來實現通用。但這種機制是不同於java之類的語言的,雖然它們都是靜態型別語言。
注意:由於本文是按照相關書籍順序編寫的,所以很多內容是具有一定侷限性的。 例如rust用於限定通用型別範圍的方式不是隻有一種。後續會補充有關內容。