詳解 Rust 的泛型

楔子

所有的編程語言都致力於將重複的任務簡單化,併爲此提供各種各樣的工具。在 Rust 中,泛型(generics)就是這樣一種工具,它是具體類型或其它屬性的抽象替代。在編寫代碼時,我們可以直接描述泛型的行爲,以及與其它泛型產生的聯繫,而無須知曉它在編譯和運行代碼時採用的具體類型。

總結一下泛型就是,提高代碼的複用能力,處理重複代碼。泛型是具體類型或者其它屬性的抽象代替,編寫的泛型代碼不是最終的代碼,而是一些模板,裏面有一些佔位符,編譯器在編譯的時候會將佔位符替換爲具體的類型。

函數中的泛型

函數中定義泛型的時候,我們需要將泛型定義在函數的簽名中:

// 這種定義方式是錯誤的,因爲 T 不在作用域中
// 我們要將其放在簽名裏面
fn func(arg: T) -> T {
    arg 
}

// 這樣做是正確的
fn func<T>(arg: T) -> T {
    arg
}

裏面的 T 就是一個泛型,它可以代表任意的類型,然後在編譯的時候會將其替換成具體的類型,這個過程叫做單態化。

另外這個 T 就是一個佔位符,你換成別的也可以,只是我們一般寫作 T。

這裏我們連續聲明瞭多個變量 x,這在 Rust 裏面是沒有問題的,因爲 Rust 有一個變量隱藏機制。然後再來看一下變量 x 的類型,雖然泛型 T 可以代表任意類型,但 Rust 在編譯的時候會執行單態化,確定泛型的具體類型。

比如傳一個 123,那麼 T 就會被標記爲 i32,因此返回的也是 i32,至於其它類型同理。還是那句話,T 只是一個佔位符,至於它到底代表什麼類型,取決於我們調用時傳遞的值是什麼類型。

比如傳遞一個 &str,那麼函數就會被 Rust 替換成如下:

fn func(arg: &str) -> &str {
    arg
}

以上過程被稱爲單態化,Rust 在編譯期間會將泛型 T 替換成具體的類型。因此如果想使用泛型,那麼函數簽名中的泛型一定要出現在函數參數中,然後根據調用方傳遞的值的類型,來確定泛型。

總結一下:泛型一定要在函數的簽名中,也就是在函數後面通過 <> 進行指定,否則的話泛型是無法使用的。此外,泛型還要出現在參數中,這是毫無疑問的,不然定義泛型幹啥。

當然啦,泛型不止可以定義一個,定義任意個都是可以的。

// 如果有多個泛型,那麼在 <> 裏面通過逗號分隔
// 然後要保證函數簽名 <> 裏面聲明的泛型,
// 都要在函數參數中出現,也就是要將定義的泛型全用上
fn func<A, B, C>(
    arg1: A, arg2: B, arg3: C
) -> (C, A) {
    (arg3, arg1)
}

fn main() {
    // 函數 func 定義了三個泛型,然後返回的類型是 (C, A)
    // 這裏傳遞三個參數,顯然當調用時,Rust 會確定泛型代表的類型
    // A 是 i32,B 是 f64,C 是 &str
    let x = func(123, 3.14, "你好");

    // 泛型可以接收任何類型,那麼當調用時
    // A 是 Vec<i32>,B 是 [i32;2],C 是 (i32, i32)
    let y = func(vec![1, 2][1, 2](3, 4));
}

這裏我們定義了三個泛型,然後返回的類型是 (C, A)。而 Rust 會根據參數的類型,來確定泛型,所以變量 x 是 (&str, i32) 類型,變量 y 是 ((i32, i32), Vec) 類型。

事實上 IDE 也已經推斷出來了,總的來說泛型應該不難理解。

結構體中的泛型

如果一個結構體成員的類型不確定,那麼也可以定義爲泛型。

struct Point<T> {
    x: T,
    y: T
}

和函數一樣,泛型一定要寫在 <> 當中作爲簽名出現,然後纔可以使用,相當於告訴 Rust 都定義了哪些泛型。然後簽名中的泛型,一定要全部使用,會根據函數調用時給參數傳的值、或者實例化結構體時給成員傳的值,來確定泛型代表哪一種類型。

如果簽名中的泛型沒有全部使用,那麼 Rust 就無法執行單態化,於是報錯。所以泛型一定要全部使用,再說了,不使用的話,定義它幹嘛。

struct Point<T> {
    x: T,
    y: T
}

fn main() {
    let p1 = Point{x: 11, y: 22};
    let p2 = Point{x: 11.1, y: 22.2};
}

T 只是一個佔位符,具體什麼類型要由我們傳遞的內容決定,可以是 i32,可以是 f64。但由於成員 x 和 y 的類型都是 T,所以它們的類型一定是一樣的,要是 i32 則都是 i32,要是 f64 則都是 f64。

如果希望類型不同,那麼只需要兩個泛型即可。

struct Point<T, U> {
    x: T,
    y: U
}

fn main() {
    // x 和 y 的類型可以相同,也可以不同
    // 因爲它們都可以接收任意類型
    let p1 = Point{x: 11, y: 22};
    let p2 = Point{x: 11.1, y: 22.2};
    let p3 = Point{x: "11.1", y: String::from("satori")};
    let p3 = Point{x: vec![1, 2, 3], y: (1, 2, 3)};
}

還是那句話,泛型可以接收任意類型,想傳啥都行,具體根據我們傳遞的值來確定。

枚舉中的泛型

枚舉也是支持泛型的,比如之前使用的 Option 就是一種泛型,它的結構如下:

enum Option<T> {
    Some(T),
    None
}

裏面的 T 可以代表任意類型,然後我們再來自定義一個枚舉。

// 簽名中的泛型參數必須都要使用
// 比如函數簽名的泛型,要全部體現在參數中
// 枚舉和結構體簽名的泛型,要全部體現在成員中
enum MyOption<A, B, C> {
    // 這裏 A、B、C 都是我們隨便定義的,可以代指任意類型
    // 具體是哪種類型,則看我們傳遞了什麼
    Some1(A),
    Some2(B),
    Some3(C),
}

fn main() {
    // 泛型不影響效率,是因爲 Rust 要進行單態化
    // 所以泛型究竟代表哪一種類型要提前確定好
    // 這裏必須要顯式指定 x 的類型。枚舉和結構體不同
    // 結構體每個成員都要賦值,所以 Rust 能夠基於賦的值推斷出所有的泛型
    // 但枚舉的話,每次只會用到裏面的一個成員
    // 如果還有其它泛型,那麼 Rust 就無法推斷了
    // 比如這裏只能推斷出泛型 C 代表的類型,而 A 和 B 就無法推斷了
    // 因此每個泛型代表什麼類型,需要我們手動指定好
    let x: MyOption<i32, f64, u8> = MyOption::Some3(123);
    match x {
        MyOption::Some1(v) => println!("我是 i32"),
        MyOption::Some2(v) => println!("我是 f64"),
        MyOption::Some3(v) => println!("我是 u8"),
    }

    // 泛型可以代表任意類型,指定啥都是可以的
    let y: MyOption<u8, i32, String> =
        MyOption::Some3(String::from("xxx"));
    match y {
        MyOption::Some1(v) => println!("我是 u8"),
        MyOption::Some2(v) => println!("我是 i32"),
        MyOption::Some3(v) => println!("我是 String"),
    }
    
    /*
    我是 u8
    我是 String
    */
}

如果覺得上面的例子不好理解的話,那麼再舉個簡單的例子:

enum MyOption<T> {
    MySome1(T),
    MySome2(i32),
    MySome3(T),
    MyNone
}

fn main() {
    // 這裏我們沒有指定 x 的類型
    // 這是因爲 MyOption 只有一個泛型
    // 通過給 MySome1 傳遞的值,可以推斷出 T 的類型
    let x = MyOption::MySome1(123);

    // 同樣的道理,Rust 可以自動推斷,得出 T 是 &str
    let x = MyOption::MySome3("123");

    // 但此處就無法自動推斷了,因爲賦值的是 MySome2 成員
    // 此時 Rust 獲取不到任何有關 T 的信息,無法執行推斷
    // 因此我們需要手動指定類型,但仔細觀察一下聲明
    // 首先,如果沒有泛型的話,那麼直接 let x: MyOption = ... 即可
    // 但裏面有泛型,所以此時除了類型之外,還要連同泛型一起指定
    // 也就是 MyOption<f64>
    let x: MyOption<f64> = MyOption::MySome2(123);

    // 當然泛型可以代表任意類型,此時的 T 則是一個 Vec<i32> 類型
    let x: MyOption<Vec<i32>> = MyOption::MySome2(123);
}

所以一定要注意:在聲明變量的時候,如果 Rust 不能根據我們賦的值推斷出泛型代表的類型,那麼我們必須要手動聲明類型,來告訴 Rust 泛型的相關信息,這樣纔可以執行單態化。

對於結構體也是同樣的道理:

struct Girl1 {
    field: i32,
}

struct Girl2<T> {
    field: T,
}

fn main() {
    // 下面兩個語句類似,只是第二次聲明 g1 的時候多指定了類型
    let g1 = Girl1 { field: 123 };
    let g1: Girl1 = Girl1 { field: 123 };

    // 下面兩條語句也是類似的,第二次聲明 g2 的時候多指定了類型
    // 但此時的類型有些不一樣,Girl2 的結尾多了一個 <i32>
    // 原因很簡單,因爲 Girl2 裏面有泛型
    // 所以在顯式指定類型的時候,還要將泛型代表的類型一塊指定,否則報錯
    let g2 = Girl2 { field: 123 };
    let g2: Girl2<i32> = Girl2 { field: 123 };
}

然後還有一點比較重要,就是在聲明的時候,只需在 <> 裏面指定泛型即可,什麼意思呢?舉個例子:

struct Girl<E, T, W> {
    field1: String,
    field2: T,
    field3: W,
    field4: E,
    field5: i32,
}

fn main() {
    // 這裏可以不指定類型,因爲 Rust 可以推斷出來
    // 不過這裏我們就顯式指定。而雖然 Girl 有 5 個成員
    // 但泛型的數量是三個,因此聲明變量的時候也要指定三個
    // 由於定義結構體的時候,泛型順序是 E T W
    // 所以這裏的 f64 就是 E,u8 就是 T,Vec<i32> 就是 W
    let g: Girl<f64, u8, Vec<i32>> = Girl {
        field1: String::from("hello"),
        field2: 123u8,
        field3: vec![1, 2, 3],
        field4: 3.14,
        field5: 666,
    };
}

以上就是在枚舉中使用泛型,並且針對泛型的用法稍微多囉嗦了一些。

方法中的泛型

我們也可以對方法實現泛型,舉個例子:

struct Point<T, U> {
    x: T,
    y: U
}

// 針對 i32、f64 實現的方法
// 只有傳遞的 T、U 對應 i32、f64 纔可以調用
impl Point<i32, f64> {
    fn m1(&self) {
        println!("我是 m1 方法")
    }
}

fn main() {
    let p1 = Point{x: 123, y: 3.14};
    p1.m1();  // 我是 m1 方法

    let p2 = Point{x: 3.14, y: 123};
    //p2.m1();
    //調用失敗,因爲 T 和 U 不是 i32、f64,而是 f64、i32
    //所以 p2 無法調用 m1 方法
}

可能有人好奇了,聲明方法的時候不考慮泛型可不可以,也就是 impl Point {}。答案是不可以,如果結構體中有泛型,那麼聲明方法的時候必須指定。但這就產生了一個問題,那就是隻有指定類型的結構體才能調用方法。

比如上述代碼,只有當 x 和 y 分別爲 i32、f64 時,纔可以調用方法,如果我希望所有的結構體實例都可以調用呢?

struct Point<T, U> {
    x: T,
    y: U
}

// 針對 K、f64 實現的方法,由於 K 是一個泛型
// 所以它可以代表任何類型(泛型只是一個符號)
// 因此不管 T 最終是什麼類型,i32 也好、&str 也罷
// K 都可以接收,只要 U 是 f64 即可
// 然後注意:如果聲明方法時結構體後面指定了泛型
// 那麼必須將使用的泛型在 impl 後面聲明
impl <K> Point<K, f64> {
    fn m1(&self) {
        println!("我是 m1 方法")
    }
}

// 此時 K 和 S 都是泛型,那麼此時對結構體就沒有要求了
// 因爲不管 T 和 W 代表什麼,K 和 S 都能表示,因爲它們都是泛型
impl <K, S> Point<K, S> {
    fn m2(&self) {
        println!("我是 m2 方法")
    }
}

// 這裏我們沒有使用泛型,所以也就無需在 impl 後面聲明
// 但很明顯,此時結構體實例如果想調用 m3 方法
// 那麼必須滿足 T 是 u8,W 是 f64
impl Point<u8, f64> {
    fn m3(&self) {
        println!("我是 m3 方法")
    }
}

fn main() {
    // 顯然 p1 可以同時調用 m1 和 m2 方法,但 m3 不行
    // 因爲 m3 要求 T 是一個 u8,而當前是 &str
    let p1 = Point{x: "hello", y: 3.14};
    p1.m1();  // 我是 m1 方法
    p1.m2();  // 我是 m2 方法

    // 顯然 p2 可以同時調用 m1、m2、m3
    // 另外這裏的 x 可以直接寫成 123,無需在結尾加上 u8
    // 因爲 Rust 看到我們調用了 m3 方法,會自動推斷爲 u8
    let p2 = Point{x: 123u8, y: 3.14};
    p2.m1();  // 我是 m1 方法
    p2.m2();  // 我是 m2 方法
    p2.m3();  // 我是 m3 方法

    // 顯然 p3 只能調用 m2 方法,因爲 m2 對 T 和 W 沒有要求
    // 但是像 m3 就不能調用,因爲它是爲 <u8, f64> 實現的方法
    // 只有當 T、W 爲 u8、f64 時纔可以調用
    // 顯然此時是不滿足的,因爲都是 &str,至於 m1 方法也是同理
    // 所以 p3 只能調用 m2,這個方法是爲 <K, S> 實現的
    // 而 K 和 S 也是泛型,可以代表任意類型,因此沒問題
    let p3 = Point{x: "3.14", y: "123"};
    p3.m2();  // 我是 m2 方法
}

然後注意:我們上面的泛型本質上針對的還是結構體,而我們定義方法的時候也可以指定泛型,其語法和在函數中定義泛型是一樣的。

#[derive(Debug)]
struct Point<T, U> {
    x: T,
    y: U,
}

// 使用 impl 時 Point 後面的泛型名稱可以任意
// 比如我們之前起名爲 K 和 S,但這樣容易亂,因爲字母太多了
// 所以建議:使用 impl 時的泛型和定義結構體時的泛型保持一致即可
impl<T, U> Point<T, U> {
    // 方法類似於函數,它是一個獨立的個體,可以有自己獨立的泛型
    // 然後返回值,因爲 Point 裏面是泛型,可以代表任意類型
    // 那麼自然也可以是其它的泛型
    fn m1<V, W>(self, a: V, b: W) -> Point<U, W> {
        // 所以返回值的成員 x 的類型是 U,那麼它應該來自於 self.y
        // 成員 y 的類型是 W,它來自於參數 b
        Point { x: self.y, y: b }
    }
}

fn main() {
    // T 是 i32,U 是 f64
    let p1 = Point { x: 123, y: 3.14 };
    // V 是 &str,W 是 (i32, i32, i32)
    println!("{:?}", p1.m1("xx"(1, 2, 3)))
    // Point { x: 3.14, y: (1, 2, 3) }
}

以上就是 Rust 的泛型,當然在工作中我們不會聲明的這麼複雜,這裏只是爲了更好掌握泛型的語法。

然後注意一下方法裏面的 self,不是說方法的第一個參數應該是引用嗎?理論上是這樣的,但此處不行,而且如果寫成 &self 是會報錯的,會告訴我們沒有實現 Copy 這個 trait。

之所以會有這個現象,是因爲我們在返回值當中將 self.y 賦值給了成員 x。那麼問題來了,如果方法的第一個參數是引用,就意味着結構體在調用完方法之後還能繼續用,那麼結構體內部所有成員的值都必須有效,否則結構體就沒法用了。這個動態數組相似,如果動態數組是有效的,那麼內部的所有元素必須都是有效的,否則就可能訪問非法的內存。

因此在構建返回值、將 self.y 賦值給成員 x 的時候,就必須將 self.y 拷貝一份,並且還要滿足拷貝完之後數據是各自獨立的,互不影響。如果 self.y 的數據全部在棧上(可 Copy 的),那麼這是沒問題的;如果涉及到堆,那麼只能轉移 self.y 的所有權,因爲 Rust 默認不會拷貝堆數據,但如果轉移所有權,那麼方法調用完之後結構體就不能用了,這與我們將第一個參數聲明爲引用的目的相矛盾。

所以 Rust 要求 self.y 必須是可 Copy 的,也就是數據必須都在棧上,這樣才能滿足在不拷貝堆數據的前提下,讓 self.y 賦值之後依舊保持有效。但問題是,self.y 的類型是 U,而 U 代表啥類型 Rust 又不知道,所以 Rust 認爲 U 不是可 Copy 的,或者說沒有實現 Copy 這個 trait,於是報錯。

因此第一個參數必須聲明爲 self,此時泛型是否實現 Copy 就不重要了,沒實現的話會直接轉移所有權。因爲該結構體實例在調用完方法之後會被銷燬,不再被使用,那麼此時可以轉移內部成員的所有權。正所謂人都沒了,還要這所有權有啥用,不如在銷燬之前將成員值的所有權交給別人。

最後說一下泛型代碼的性能,使用泛型的代碼和使用具體類型的速度是一樣的,因此這就要求 Rust 在編譯的時候能夠推斷出泛型的具體類型,所以類型要明確。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/rvasBwKz8CvbTFT9wRjtDg