rust學習十一.1、泛型(通用型別)

正在战斗中發表於2024-11-21

這是和大部分的語言差不多的一個概念,只不過實現上有一些區別而已。

所以,如果學過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的,必須定義為:

largest<T:std::cmp::PartialOrd>(list: &[T]) -> &T
這是為了限定T的型別範圍,否則會報告異常,具體什麼異常,需要看情況而定。
這個例子告訴我們:rust也面臨其它語言一樣的問題,這個問題就是在某些場合需要限定通用型別的型別範圍,因為有的行為不適合於所有型別。
在本文中,限定型別範圍的方式只有這個。
但實際上,rust有多種方式,後續會補充完善!

四、在執行時判斷通用型別的實際型別

rust好像不支援這個。

rust是靜態型別語言,這意味著它在編譯的時候,就需要確定變數例項的型別。

而這也意味著,它不怎麼需要在執行時候:動態判定一個變數例項的具體的型別。

如果它那麼做,可能會違法它的核心理念:安全之上。

說到這個問題,不得不提到java。

java雖然是名義上的靜態型別語言,但是由於java的特殊性(一切都是類),它有辦法透過為物件設定屬性來判斷一個例項的型別。

java可以利用instaneof來判斷。

rust沒有這種機制,至少學到這裡是沒有的!

然而rust也有變通的方式改變這個,此處不論!

五、編譯與效能

這個比較有意思。

在入門級的書籍中,我們就能瞭解到這個事實:

  1. 泛型並不會使程式比具體型別執行得慢
  2. 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採用靜態實現,為什麼不支援動態判斷變數例項的型別(待確認)。

六、小結

  1. 通用型別針對的是物件屬性,方法/函式引數,不是物件、方法本身。這個基本同java等語言
  2. 可以使用合法的識別符號來表示通用型別,不必一定是K,T,V之類的,包括單詞,漢語詞語等。這個方面和JAVA等語言類似
  3. rust並沒有對方法/函式一或者物件可以定義的通用型別進行個數限制
  4. 如果要對函式返回型別使用通用型別,則必須在函式名後跟上諸如<T>這樣的字元,這個方式和java的類似。當方法不需要(注意函式和方法的區別)
  5. rust一樣存在通用型別的常見問題:要麼定義的時候限定通用型別的型別,要麼是執行時在方法/函式體中使用類似 instanceof(java)之類的判斷泛型的實際型別。rust只能在編碼的時候進行限制
  6. 但rust可以使用類似 T:xxxx的方式限定型別範圍,或者T:xxxx+****+???+..,,其中xxxx,****,???都是表示特定型別/操作
  7. 截止本章為止,rust並沒有?這樣的萬用字元,可以用於通用型別中。java有類似 ? super/extends Grade這樣的用法,可用於限定引數型別
  8. rust透過在編譯階段的行為(單態化/具體化)來實現通用。但這種機制是不同於java之類的語言的,雖然它們都是靜態型別語言。

注意:由於本文是按照相關書籍順序編寫的,所以很多內容是具有一定侷限性的。 例如rust用於限定通用型別範圍的方式不是隻有一種。後續會補充有關內容。

相關文章