Rust 語法梳理與總結(中)

楔子

我們繼續來梳理 Rust 語法,本次讓我們把目光聚焦於函數、閉包、泛型和 trait 上面,開始接下來的內容。並且在梳理的時候,同樣會對之前沒有說的內容做一個補充。

函數

函數(function)使用 fn 關鍵字來聲明,函數的參數需要標註類型,就和變量一樣。如果函數返回一個值,返回值的類型必須在箭頭 -> 之後指定。

函數的最後一個表達式將作爲返回值,當然也可以在函數內使用 return 語句來提前返回一個值。

// 一個返回布爾值的函數,判斷 b 能夠被 a 整除
fn is_divisible_by(a: u32, b: u32) -> bool {
    if b == 0 {
        false
    } else {
        a % b == 0
    }
}

fn main() {
    println!("{}", is_divisible_by(5, 2));
    println!("{}", is_divisible_by(5, 0));
    println!("{}", is_divisible_by(6, 3));
    /*
    false
    false
    true
    */
}

然後函數參數默認都是不可變的,如果希望它可變,那麼在聲明的時候也要使用 mut 關鍵字。

fn is_divisible_by(a: u32, mut b: u32) -> bool {
    // 此時 b 就是一個可變參數
    // 如果 b 等於 0,那麼就將它改成 1
    if b == 0 {
        b = 1
    }
    a % b == 0
}

fn main() {
    println!("{}", is_divisible_by(5, 2));
    println!("{}", is_divisible_by(5, 0));
    println!("{}", is_divisible_by(6, 3));
    /*
    false
    true
    true
    */
}

既然提到了函數,那麼就不得不提方法,方法是依附於某個對象的函數,在 impl 代碼塊內定義。

#[derive(Debug)]
struct Point {
    x: f64,
    y: f64,
}

// 實現的代碼塊
impl Point {
    // 這是一個靜態方法(static method)
    // 靜態方法不需要被實例調用
    // 這類方法一般用作構造器(constructor)
    fn origin() -> Point {
        Point { x: 0.0, y: 0.0 }
    }

    // 另一個靜態方法,需要兩個參數:
    fn new(x: f64, y: f64) -> Point {
        Point { x: x, y: y }
    }

    // 計算兩個點之間的曼哈頓距離
    fn distance(&self, other: &Point) -> f64 {
        (self.x - other.x).abs() + (self.y - other.y).abs()
    }

    // 修改某個點的座標
    fn modify_x(&mut self, x: f64) {
        self.x = x;
    }
    fn modify_y(&mut self, y: f64) {
        self.y = y;
    }
}

fn main() {
    let mut p1 = Point::new(3.2, 1.4);
    let mut p2 = Point::new(4.3, 1.5);

    println!("{:?}", p1);
    println!("{:?}", p2);
    /*
    Point { x: 3.2, y: 1.4 }
    Point { x: 4.3, y: 1.5 }
    */

    println!("{}", p1.distance(&p2));
    println!("{}", p2.distance(&p1));
    /*
    1.1999999999999997
    1.1999999999999997
    */

    p1.modify_x(10.0);
    p2.modify_y(10.0);
    println!("{:?}", p1);
    println!("{:?}", p2);
    /*
    Point { x: 10.0, y: 1.4 }
    Point { x: 4.3, y: 10.0 }
    */
}

函數和方法本身還是很簡單的,我們重點來看一下閉包。

閉包

閉包包含兩部分:

我們先來看看普通函數和匿名函數在語法上的區別。

fn main() {
    fn add1(a: i32, b: i32) -> i32 {
        a + b
    }
    let add2 = |a: i32, b: i32| -> i32 {
        a + b
    };
}

使用 fn 關鍵字定義的是普通函數,而匿名函數沒有函數名,並且要把參數所在的小括號換成 |...|。然後這裏我們將匿名函數賦值給了變量 add2,因此在使用上,add1 和 add2 沒有區別,普通函數和匿名函數都是函數。

再來看看閉包,閉包本質上也是一個匿名函數,但它可以捕獲外層作用域的變量,而這是 fn 定義的普通函數所做不到的。

fn main() {
    let start = 1;
    let add = |a: i32, b: i32| -> i32 {
        a + b + start
    };

    println!("{}", add(2, 1));  // 4
}

因爲變量 start 是定義在 main 函數里面的,所以閉包 add 捕獲了外層作用域(這裏是 main 函數的作用域)的變量。而我們說普通函數做不到這一點,來驗證一下。

我們都不用執行,IDE 就已經給出提示了。普通函數無法捕獲外層作用域的變量,我們應該使用 **|| {...}**定義的閉包。

捕獲變量會產生額外開銷

然後閉包還可以省略參數的類型,以及返回值類型。

fn main() {
    // 不指定參數的類型和返回值類型
    // Rust 會根據我們後續的使用推斷出來
    let add = |i, j| {
        (i, j)
    };
    // 這裏調用了兩次 add,顯然 Rust 會進行推斷
    // 會將參數 i 推斷爲 f32,將 j 推斷爲 u16
    // 然後返回的是 (i, j),那麼返回值類型就是 (f32, u16)
    println!("{:?}", add(3.14, 666u16));
    println!("{:?}", add(2.71f32, 777));
    /*
    (3.14, 666)
    (2.71, 777)
    */

    // 一旦推斷出來,那麼參數和返回值類型就確定了
    // 所以下面的調用是不合法的,因爲參數類型錯了
    // add(12, 13);  // 報錯,將 12 改成 12.0,那麼就沒問題了
}

總的來說,Rust 的類型推斷還是很智能的,但即便如此,我們也最好明確指定類型。

另外,如果代碼塊只有一行代碼的話,那麼大括號也可以省略。

fn main() {
    let add = |i, j| (i, j);
    println!("{:?}", add(3.14, 666u16));
    println!("{:?}", add(2.71f32, 777));
    /*
    (3.14, 666)
    (2.71, 777)
    */
}

注意:只有閉包纔可以這麼幹,普通函數是不可以的。不過爲了代碼的可讀性,還是不建議將大括號去掉,另外參數和返回值的類型最好也不要省略。

將閉包作爲參數

先來回顧一下函數是怎麼作爲參數的。

type F = fn(i32, i32) -> i32;
// calc 接收三個參數
// 第一個參數:接收兩個 i32 返回一個 i32 的函數
// 第二個參數和第三個參數均是 i32
fn calc(op: F, a: i32, b: i32) -> i32 {
    op(a, b)
}

fn main() {
    let add = |a: i32, b: i32| -> i32 {
        a + b
    };
    println!("{}", calc(add, 11, 22));  // 33
}

咦,參數 op 接收的是普通函數,而我們傳遞的 add 是一個閉包,爲啥沒報錯呢?很簡單,因爲在 Rust 裏面閉包本質上就是一個匿名函數,只不過它可以捕獲外層作用域的變量罷了。但我們這裏沒有捕獲,因此它和 fn 定義的普通函數是完全等價的,都是函數。如果我們將代碼修改一下:

我們看到報錯了,告訴我們類型不匹配。由於使用了外部的變量 start,所以閉包 add 就不能作爲參數傳遞了,此時它和 fn 定義的普通函數不是完全等價的,因爲後者無法捕獲外層作用域的變量。

所以我們要修改函數 calc 的第一個參數的類型,將它改成閉包類型,而這需要使用一個叫 Fn 的 trait 來實現(一會兒介紹 trait)。

fn calc<T>(op: T, a: i32, b: i32) -> i32
where
    T: Fn(i32, i32) -> i32,
{
    op(a, b)
}
// 或者你還可以這麼定義:
/*
fn calc(op: impl Fn(i32, i32) -> i32,
        a: i32, b: i32) -> i32 {}
*/

fn main() {
    let start = 1;
    let add = |a: i32, b: i32| -> i32 {
        a + b + start
    };
    println!("{}", calc(add, 11, 22)); // 34
    println!("{}", add(11, 22)); // 34
}

以上我們就將閉包作爲參數傳遞了,另外傳遞一個 fn 關鍵字定義的普通函數也是可以的。雖然它不是閉包,但它實現了 Fn 這個 trait。

fn calc<T>(op: T, a: i32, b: i32) -> i32
where
T: Fn(i32, i32) -> i32,
{
    op(a, b)
}

fn main() {
    let add = |a: i32, b: i32| -> i32 {
        a + b
    };
    println!("{}", calc(add, 11, 22)); // 33

    fn add(a: i32, b: i32) -> i32 {
        a + b
    }
    println!("{}", calc(add, 11, 22)); // 33
}

在 Rust 裏面,不管有沒有捕獲外層作用域的變量,匿名函數都是一個閉包。只是在沒有捕獲的情況下,它和 fn 定義的普通函數也是完全等價的。

閉包作爲返回值

再來看看如何將閉包作爲返回值,這中間會有一些和其它語言不一樣的地方。

fn login(username: String, passwd: String) -> impl Fn() -> String {
    move || {
        if username.eq(&String::from("satori")) &&
           passwd.eq(&String::from("123456")){
            return String::from("歡迎來到編程教室")
        } else {
            return String::from("用戶名密碼錯誤")
        }
    }
}

fn main() {
    let username = String::from("satori");
    let passwd = String::from("123456");
    let closure = login(username, passwd);
    println!("{}", closure());
    /*
    歡迎來到編程教室
    */

    let username = String::from("satori");
    let passwd = String::from("654321");
    let closure = login(username, passwd);
    println!("{}", closure());
    /*
    用戶名密碼錯誤
    */
}

代碼非常簡單,調用 login 函數之後返回一個閉包,然後調用閉包的時候比較用戶名和密碼,返回不同的字符串。需要注意的是,字符串的 eq 方法的第一個參數接收的是引用,所以閉包中調用 username.eq 和 passwd.eq 時,拿到的是 username 和 passwd 兩個字符串的引用,而不是字符串本身。

但問題是相比之前,這段閉包代碼多了一個 move 關鍵字,這是幹啥用的。

我們仔細想一下,當閉包返回之後,login 函數是不是調用結束了,它的相關參數是不是也就銷燬了呢。那麼後續調用閉包的時候,再使用 username 和 passwd 就會報錯,因爲引用的對象已經被銷燬了。

於是 Rust 提供了 move 關鍵字,會將捕獲的外部變量移動到閉包內部,確保閉包在調用的時候能夠找得到。如果變量是可 Copy 的,那麼移動的時候會拷貝一份;如果不是可 Copy 的,那麼移動的時候就轉移所有權。

我們看這個例子,定義閉包的時候使用了 move 關鍵字,那麼它會將外部變量,移動到閉包內部。所以在閉包的下面再使用 name 變量就會報錯,因爲所有權已經被轉移了。

至於 age 則沒有關係,因爲它是可 Copy 的,這種變量的數據都完全分配在棧上,不涉及到堆。而棧上數據在傳遞的時候只會拷貝一份,所以傳遞之後你的是你的,我的是我的,兩者沒有關係,自然也就不會出現所有權的轉移,因爲數據都是各自獨立的。

但對於涉及到堆內存的變量來說就不一樣了,由於 Rust 默認不會拷貝堆上數據,那麼變量在傳遞之後就會出現兩個棧上指針,指向同一份堆內存。於是 Rust 就會轉移所有權,只讓一個變量擁有操作堆內存的權利。所以 name 在移動之後就不能再用了,因爲它已經失去了操作堆內存的權利。

當然啦,對於當前這個例子,即使不使用 move 也是可以的,因爲這是在 main 函數里面,閉包的存活時間沒有超過它所捕獲的變量。但之前那個例子不行,之前那個例子的閉包使用了 login 函數的兩個參數,而在參數伴隨 login 函數調用結束而銷燬之後,閉包卻依舊存活,因此報錯。

Rust 閉包的實現和所有權的實現,在語義上是統一的。

Fn、FnOnce、FnMut

Rust 一切皆類型,並由 trait 掌握類型的行爲邏輯。至於閉包類型則需要通過 Fn trait 來指定,但除了 Fn 之外,還有兩個 trait,也就是 FnOnce 和 FnMut。那麼它們之間有什麼區別呢?

FnOnce

實現此 trait 的閉包在使用外部變量的時候,可以奪走所有權。

我們看到兩個函數的函數體都是一樣的,但使用 Fn trait 就有問題,而使用 FnOnce trait 則一切正常。原因就是對於 FnOnce 來說,它可以奪走外部變量的所有權,而 Fn 不能。

fn test() -> impl FnOnce() {
    let name = String::from("古明地覺");
    || {
        let new_name = name;
    }
}

fn main() {
    // 拿到閉包,test 裏變量 name 的所有權,
    // 會轉移給閉包內部的 new_name 變量
    let closure = test();
    // 調用閉包,結束之後 name 被回收
    closure();
    // 如果再次調用,那麼就會報錯
    // closure()  // 報錯,name 已被釋放
}

所以這也是它叫 FnOnce 的原因,就是因爲它在剝奪外部變量所有權的時候只能調用一次。但如果沒有轉移所有權,那麼需要使用 move 關鍵字將變量移動到閉包內部,舉個例子:

fn test1() -> impl FnOnce() {
    let name = String::from("我是閉包");
    // 在閉包中將 name 賦值給了 new_name
    // 顯然所有權會發生轉移,這是很合理的
    || {
        let new_name = name;
    }
}

fn test2() -> impl FnOnce() {
    let name = String::from("我是閉包");
    // 由於閉包內部用的都是 name 的引用
    // 所以這時候不會發生所有權的轉移
    // 但很明顯這種情況是會出問題的
    // 因爲後續使用閉包的時候,name 已經被銷燬了
    // 所以這裏要通過 move,手動地將變量轉移至閉包內部
    move || {  
        let new_name = &name;  // name 的引用
        // 這裏拿到的也是 name 的引用
        println!("{}", name);
    }
}

所以實現 FnOnce trait 的閉包,能夠轉移捕獲的外部變量的所有權。而如果不涉及所有權的轉移,也就是通過引用的方式使用了外部變量,那麼我們需要通過 move 關鍵字將變量移動到閉包內部,否則後續使用閉包的時候就會出問題。

注意:我們上面捕獲的變量是 String 類型,它沒有實現 Copy trait,所以一直反覆提到所有權。但如果捕獲的變量實現了 Copy trait,那麼一定要加 move,將變量移動到閉包內部。

Fn

實現此 trait 的閉包在使用外部變量的時候,需要滿足一個條件:不可以在閉包內部奪走外部變量的所有權,必須以不可變引用的方式進行操作。

IDE 提示的很明顯,我們不能在閉包內部轉移外部變量的所有權,即便使用 move 關鍵字將變量移動到了閉包內部。不過對於實現了 Copy trait 的變量來說是可以的,因爲我們說這種變量的數據都完全分配在棧上,傳遞之後數據都是各自獨立的,不存在所有權的轉移。

但對於涉及到堆內存的變量來說就不一樣了,由於 Rust 默認不會拷貝堆上數據,於是會轉移所有權。但對於實現 Fn trait 的閉包,Rust 不允許轉移外部變量的所有權,因此只能以引用的方式操作。

fn test() -> impl Fn() {
    let name = "satori".to_string();
    let age = 17;
    let c = move || {
        // 雖然在定義閉包的時候,將name移動到了閉包內部
        // 但在使用 name 的時候,仍必須以引用的方式
        // 否則它的所有權就會轉移給 new_name
        // 而 Rust 不允許這種情況出現
        let new_name = &name;
        // age 是可 Copy 的,不涉及所有權的轉移
        // 因此使用 age 和 &age 均可
        let new_age = age;
    };
    // age 依然有效,它是可 Copy 的
    // 在移動的時候會拷貝一份,所以不影響
    println!("{}", age);
    // 但 name 就不行了,因爲它被移動到了閉包內部
    c
}

所以 Fn、FnOnce、FnMut 描述的都是閉包將以何種方式去捕獲外部變量。對於當前的 Fn 來說,只能以引用的方式去捕獲,而不能奪走所有權,即使我們使用 move 關鍵字將變量移動到了閉包內部。

FnMut

它和 Fn 一樣都只能獲取外部變量的引用,但 Fn 在使用的時候只能拿到不可變引用,而 FnMut 還可以獲取可變引用。

test1 函數是不合法的,實現 Fn trait 的閉包不可以獲取外部變量的可變引用。

fn test() -> impl FnMut() -> String {
    let mut name = "komeiji".to_string();
    move || {
        let new_name = &mut name;
        new_name.push_str(" satori");
        name.clone()
    }
}

fn main() {
    // 這裏閉包也要聲明爲可變的
    let mut c = test();
    println!("{}", c());
    println!("{}", c());
    println!("{}", c());
    /*
    komeiji satori
    komeiji satori satori
    komeiji satori satori satori
    */
    // name 的所有權自始至終在閉包內部
    // 每一次調用都會對其進行修改
}

注意這段代碼,裏面有幾個需要注意的地方。我們在閉包內部創建變量 new_name 的時候,只能將 name 的引用賦值給它,而不能是 name 本身,那樣的話所有權就發生轉移了。然後要修改字符串,那麼 name 必須可變,new_name 也要是可變引用。

最後我們返回了 name.clone(),問題來了,直接返回 name 不行嗎?答案是不行,對於 Fn 和 FnMut 來說,Rust 不允許在閉包內部轉移外部變量的所有權,縱使這個外部變量被移動到了閉包內部。所以返回 name 是不合法的,因爲那樣所有權就發生轉移了。

再來寫一個計數器的例子,我們先看用 Go 如何去寫:

package main

import (
    "fmt"
)

// ticker 函數返回了一個閉包
// 閉包在調用時會返回一個 uint32
// 而閉包返回的 uint32 來自 ticker 裏的 start 變量
func ticker() func() uint32 {
    var start uint32 = 0
    return func() uint32 {
        start++
        return start
    }
}
func main() {
    var tk = ticker()
    fmt.Println(tk()) // 1
    fmt.Println(tk()) // 2
    fmt.Println(tk()) // 3
}

還是很簡單的,Go 的變量沒啥可變不可變,所以讀起來就非常簡單,然後是 Rust。

fn ticker() -> impl FnMut() -> u32 {
    // 後續要修改 start,start 必須可變
    let mut start = 0;
    // 閉包也要是可變的。然後還要使用 move 關鍵字
    // 將 start 移動到閉包內部,這是必須的
    // 否則後續使用閉包的時候,會發現 start 已被銷燬
    let mut c = move || {
        start += 1;
        start
    };
    c
}

fn main() {
    // 想在閉包內修改外部變量,則需要使用 FnMut
    // 拿到閉包之後,也要聲明爲可變的
    let mut c = ticker();
    println!("{}", c());
    println!("{}", c());
    println!("{}", c());
    /*
    1
    2
    3
    */
}

以上我們就用 Rust 實現了一個定時器。

關於 Fn、FnOnce、FnMut 我們再做一下總結:

另外爲避免出現懸空引用,記得使用 move 關鍵字將變量移動到閉包內部。一般來說,如果你將閉包作爲了返回值,那麼基本上都會使用 move。

泛型

再來看一看泛型,泛型可以出現在結構體、枚舉、函數、方法等結構中,用於增強類型的表達能力,以及減少重複代碼。

// 結構體使用泛型
struct Point<T, W> {
    x: T,
    y: W,
    z: i32
}
// 枚舉使用泛型
enum Cell<T> {
    Foo(T),
    Bar(String)
}

// 函數使用泛型
fn func<T>(a: T, b: T) {}

用到了哪些泛型,必須提前用尖括號聲明好。

struct T;

// S1 的第一個元素的類型爲單元結構體 T
struct S1(T);

// S2 的第一個元素的類型爲泛型 T
// 這個 T 可以代表任意類型,至於具體代表哪一種
// 則看在實例化的時候,傳遞的值是什麼
struct S2<T>(T);

我們來實例化測試一下:

struct T;
struct S1(T);
struct S2<T>(T);

fn main() {
    let s1 = S1(T{});
    // 對單元結構體來說,寫成 T 也是可以的
    // 這裏我們將類型也指定一下
    let s1: S1 = S1(T);

    // 然後是 S2 的實例化
    let s2 = S2(123);
    let s2 = S2("hello");
    let s2 = S2(String::from("hello"));
    // 由於 S2 接收的是泛型 T,所以它可以代表任意類型
    // 這裏的 T 是單元結構體 T
    // 如果沒有定義這個單元結構體,那麼這裏就會報錯
    let s2 = S2(T);

    // 然後是指定類型,在指定類型的同時
    // 還要指定泛型所代表的類型
    let s2: S2<u8> = S2(33);
    let s2: S2<&str> = S2("hello");
    let s2: S2<Vec<f32>> = S2(vec![3.14, 2.71, 1.414]);
}

因此這便是泛型,它可以代表任意類型,這樣極大地增強了數據的表達能力。接下來,我們將帶泛型的結構體放到函數參數裏面。

struct S<T>(T);

// 因爲結構體 S 帶了一個泛型 T,
// 所以在指定類型的時候,光有一個 S 是不夠的
// 我們還要將泛型指定好,比如 S<u32>,
// 那麼就只有當 S 實例的第一個元素爲 u32 類型時,
// 才能傳給這裏的 s 參數
fn f1(s: S<u32>) {
    println!("f1 被調用")
}
// 針對的是 S(f64)
fn f2(s: S<f64>) {
    println!("f2 被調用")
}

fn main() {
    f1(S(123));  // f1 被調用
    f2(S(3.14)); // f2 被調用
}

然後函數自身也是可以有泛型的。

struct S<T>(T);

// 泛型只是一個符號而已,它可以代表任意類型
// 但具體是哪一種,則取決於我們傳遞的值
// 另外這裏爲了區分,刻意寫成了 W,但寫成 T 仍是可以的
fn f<W>(s: S<W>) {
    println!("函數被調用")
}

fn main() {
    f(S(123));
    f(S(3.14));
    f(S("hello"));
    /*
    函數被調用
    函數被調用
    函數被調用
    */
}

函數 f 帶了一個泛型 W,而參數 s 的類型是 S。這就說明在調用函數的時候,只要傳一個結構體 S 的實例即可,而不必管元素是什麼類型,因爲泛型可以代表任意類型。

以上在確定泛型的時候,都是根據值推斷出來的,我們也可以顯式地告訴 Rust。

fn test<T>(s: T) {
    println!("函數被調用")
}

fn main() {
    // 調用時 T 會代表 i32
    test(123);
    // 調用時 T 會代表 u8
    test(123u8);
    test::<u8>(123);
}

通過 test:: 則可以告訴編譯器,在調用的時候,泛型 T 代表的是 u8 類型。當然啦,此時傳遞的也必須是一個合法的 u8 類型的值。

泛型不影響性能,Rust 在編譯期間會將泛型 T 替換成具體的類型,這個過程叫做單態化。

方法中的泛型

再來看看方法中的泛型。

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 在編譯的時候能夠推斷出泛型的具體類型,所以類型要明確。

枚舉中的泛型

枚舉也是支持泛型的,比如之前使用的 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 泛型的相關信息,這樣纔可以執行單態化。

trait

Rust 一切皆類型,並由 trait 掌握類型的行爲邏輯,我們看個例子。

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

impl<T> Point<T> {
    fn m(&self) {
        let var = self.x;
    }
}

fn main() {
    let p = Point { x: 123 };
}

你覺得這段代碼有問題嗎?不用想,肯定是有問題的。因爲方法 m 的第一個參數是引用,這就意味着方法調用完畢之後,結構體實例依舊保持有效,也意味着實例的所有成員值都保持有效。

但在方法 m 裏面,我們將成員 x 的值賦給了變量 var。如果成員 x 的類型不是可 Copy 的,也就是數據不全在棧上,還涉及到堆,那麼就會轉移所有權,因爲 Rust 默認不會拷貝堆數據。所以調用完方法 m 之後,成員 x 的值不再有效,進而使得結構體不再有效。

所以 Rust 爲了避免這一點,在賦值的時候強制要求 self.x 的類型必須是可 Copy 的,但泛型 T 可以代表任意類型,它不滿足這一特性。或者說 T 最終代表的類型是不是可 Copy 的,Rust 是不知道的,所以 Rust 乾脆認爲它不是可 Copy 的。

那麼問題來了,雖然 T 可以代表任意類型,但如果我們賦的值決定了 T 代表的類型一定是可 Copy 的,那麼可不可以告訴 Rust,讓編譯器按照可 Copy 的類型來處理呢?答案是可以的,而實現這一功能的機制就叫做 trait。

給泛型施加約束

trait 類似於 Go 裏面的接口,相當於告訴編譯器,某種類型具有哪些可以與其它類型共享的功能。

#[derive(Debug)]
struct Point<T> {
    x: T,
}
// 這裏便給泛型 T 施加了一個 Copy 的約束
// 此時的 T 代表的是實現了 Copy trait 的任意類型
impl<T: Copy> Point<T> {
    fn m(&self) {
        let var = self.x;
    }
}

fn main() {
    let p = Point { x: 123 };
}

此時這段代碼就沒問題了,因爲 T 是可 Copy 的。並且當調用 p.m 的時候,編譯器會檢查成員 x 的值是不是可 Copy 的,如果不是,則不允許調用 p.m 方法。

以上是在定義方法時,在 impl 塊裏面指定了 trait,但結構體裏面的泛型也是可以指定 trait 的。

use std::fmt::Display;
use std::fmt::Debug;

// 成員 x 的類型要實現 Display
// 成員 y 的類型要實現 Debug
struct Point<T: Display, W: Debug> {
    x: T,
    y: W
}

fn main() {
    // Display 要求變量能夠以 "{}" 的方式打印
    // Debug 要求變量能夠以 "{:?}" 的方式打印
    let s1 = Point{x: 123, y: 345};
    let s2 = Point{x: 123, y: (1, 2, 3)};
    // 下面的 s3 不合法,因爲元組沒有實現 Display
    // let s3 = Point{x: (1, 2, 3), y: 123};
}

所以這就是 trait,它描述了類型的行爲。如果我們希望某個類型的變量不受限制,但必須滿足某個特性,那麼便可以給泛型施加一些制約。比如定義一個函數,接收兩個相同類型的變量,變量具體是什麼類型不重要,只要能夠比較大小即可。

fn compare<T: PartialOrd>(a: T, b: T) -> String {
    if a > b {
        String::from("大於")
    } else if a < b {
        String::from("小於")
    } else {
        String::from("等於")
    }
}
fn main() {
    println!("{}", compare(1, 2));
    println!("{}", compare(3.14, 2.71));
    println!("{}", compare("a""b"));
    println!("{}", compare("你好""你好"));
    /*
    小於
    大於
    小於
    等於
    */
}

如果 compare 裏面的 T 不使用 trait,那麼它可以代表任意類型,顯然會報錯,因爲不是所有類型的變量都能夠比較。於是便可以使用 trait 給 T 施加一些約束,PartialOrd 表示 T 代表的類型必須能夠比較。

如果在調用 compare 時,傳遞的值不能比較,那麼編譯器就能夠檢查出來,從而報錯。

自定義 trait

標準庫內置了很多 trait,但我們也可以自定義 trait。

#[derive(Debug)]
struct Girl {
    name: String,
    age: i32
}

// trait 類似 Go 裏面的接口
// 然後裏面可以定義一系列的方法
// 這裏我們創建了一個名爲 Summary 的 trait
// 並在內部定義了一個 summary 方法
trait Summary {
    // trait 裏面的方法只需要寫聲明即可
    fn summary(&self) -> String;
}

// Go 裏面只要實現了接口裏的所有方法,便實現了該接口
// 但是在 Rust 裏面必須顯式地指明實現了哪一個 trait
// impl Summary for Girl 表示爲類型 Girl 實現 Summary 這個 trait
impl Summary for Girl {
    fn summary(&self) -> String {
        // format! 宏用於拼接字符串,它的語法和 println! 一樣
        // 並且這兩個宏都不會獲取參數的所有權
        // 比如這裏的 self.name,format! 拿到的只是引用
        format!("name: {}, age: {}", self.name, self.age)
    }
}
fn main() {
    let g = Girl{name: String::from("satori"), age: 16};
    println!("{}", g.summary());  // name: satori, age: 16
}

所以 trait 裏面的方法只需要寫上聲明即可,實現交給具體的結構體來做。當然啦,trait 裏面的方法也是可以有默認實現的。

#[derive(Debug)]
struct Girl {
    name: String,
    age: i32
}

trait Summary {
    // 我們給方法指定了具體實現
    fn summary(&self) -> String {
        String::from("hello")
    }
}

impl Summary for Girl {
    // 如果要爲類型實現 trait,那麼要實現 trait 裏面所有的方法
    // 這一點和 Go 的接口是相似的,但 Go 裏面實現接口是隱式的
    // 只要你實現了某個接口所有的方法,那麼默認就實現了該接口
    // 但在 Rust 裏面,必須要顯式地指定實現了哪個 trait
    // 同時還要實現該 trait 裏的所有方法

    // 但 Rust 的 trait 有一點特殊,Go 接口裏面的方法只能是定義
    // 而 trait 裏面除了定義之外,也可以有具體的實現
    // 如果 trait 內部已經實現了,那麼這裏就可以不用實現
    // 不實現的話則用 trait 的默認實現,實現了則調用我們實現的

    // 因此這裏不需要定義任何的方法,它依舊實現了 Summary 這個 trait
    // 只是我們仍然要通過 impl Summary for Girl 顯式地告訴 Rust
    // 如果只寫 impl Girl,那麼 Rust 則不認爲我們實現了該 trait
}
fn main() {
    let g = Girl{name: String::from("satori"), age: 16};
    // 雖然沒有 summary 方法,但因爲實現了 Summary 這個 trait
    // 而 trait 內部有 summary 的具體實現,所以不會報錯
    // 但如果 trait 裏面的方法只有聲明沒有實現,那麼就必須要我們手動實現了
    println!("{}", g.summary());  // hello
}

總結一下就是 trait 裏面可以有很多的方法,這個方法可以只有聲明,也可以同時包含實現。如果要爲類型實現某個 trait,那麼要通過 impl xxx for 進行指定,並且實現該 trait 內部定義的所有方法。但如果 trait 的某個方法已經包含了具體實現,那麼我們也可以不實現,會使用 trait 的默認實現。

trait 作爲參數

假設有一個函數,只要是實現了 info 方法,都可以作爲參數傳遞進去,這時候應該怎麼做呢?

struct Girl {
    name: String,
    age: i32,
}

struct Boy {
    name: String,
    age: i32,
    salary: u32,
}

trait People {
    fn info(&self) -> String;
}

// 爲 Girl 和 Boy 實現 People 這個 trait
impl People for Girl {
    fn info(&self) -> String {
        format!("{} {}"&self.name, self.age)
    }
}
impl People for Boy {
    fn info(&self) -> String {
        format!("{} {} {}"&self.name, self.age, self.salary)
    }
}

// 定義一個函數,注意參數 p 的類型
// 如果是 p: xxx,則表示參數 p 的類型爲 xxx
// 如果是 p: impl xxx,則表示參數 p 的類型任意,只要實現了xxx這個trait即可
fn get_info(p: impl People) -> String {
    p.info()
}

fn main() {
    let g = Girl {
        name: String::from("satori"),
        age: 16,
    };
    let b = Boy {
        name: String::from("可憐的我"),
        age: 26,
        salary: 3000,
    };
    // 只要實現了 People 這個 trait
    // 那麼實例都可以作爲參數傳遞給 get_info
    println!("{}", get_info(g)); // satori 16
    println!("{}", get_info(b)); // 可憐的我 26 3000
}

然後以 trait 作爲參數的時候,還有另外一種寫法,我們之前一直在用:

// 如果是 <T> 的話,那麼 T 表示泛型,可以代表任意類型
// 但這裏是 <T: People>,那麼就不能表示任意類型了
// 它表示的應該是實現了 People 這個 trait 的任意類型
fn get_info<T: People>(p: T) -> String {
    p.info()
}

以上兩種寫法是等價的,但是第二種寫法在參數比較多的時候,可以簡化長度。

fn get_info<T: People>(p1: T, p2: T) -> String {

}
// 否則話要這麼寫
fn get_info(p1: impl People, p2: impl People) -> String {

}

當然啦,一個類型並不僅僅可以實現一個 trait,而是可以實現任意多個 trait。

struct Girl {
    name: String,
    age: i32,
    gender: String
}

trait People {
    fn info(&self) -> String;
}

trait Female {
    fn info(&self) -> String;
}

// 不同的 trait 內部可以有相同的方法
impl People for Girl {
    fn info(&self) -> String {
        format!("{} {}"&self.name, self.age)
    }
}

impl Female for Girl {
    fn info(&self) -> String {
        format!("{} {} {}"&self.name, self.age, self.gender)
    }
}

// 這裏在 impl People 前面加上了一個 &
// 表示調用的時候傳遞的是引用
fn get_info1(p: &impl People) {
    println!("{}", p.info())
}

fn get_info2<T: Female>(f: &T) {
    println!("{}", f.info())
}

fn main() {
    let g = Girl {
        name: String::from("satori"),
        age: 16,
        gender: String::from("female")
    };
    get_info1(&g);  // satori 16
    get_info2(&g);  // satori 16 female
}

不同 trait 內部的方法可以相同也可以不同,而 Girl 同時實現了 People 和 Female 兩個 trait,所以它可以傳遞給 get_info1,也可以傳遞給 get_info2。然後爲 trait 實現了哪個方法,就調用哪個方法,所以兩者的打印結果不一樣。

那麼問題來了,如果我在定義函數的時候,要求某個參數同時實現以上兩個 trait,該怎麼做呢?

// 我們只需要使用 + 即可
// 表示參數 p 的類型必須同時實現 People 和 Female 兩個 trait
fn get_info1(p: impl People + Female) {
    // 但由於 Poeple 和 Female 裏面都有 info 方法
    // 此時就不能使用 p.info() 了,這樣 Rust 不知道該使用哪一個
    // 應該採用下面這種做法,此時需要手動將引用傳過去
    People::info(&p);
    Female::info(&p);
}

// 如果想接收引用的話,那麼需要這麼聲明
// 因爲優先級的原因,需要將 impl People + Female 整體括起來
fn get_info2(p: &(impl People + Female)) {}

// 或者使用泛型的寫法
fn get_info3<T: People + Female>(p: T) {}

最後還有一個更加優雅的寫法,在介紹閉包的時候用過:

// 顯然這種聲明方式要更加優雅,如果沒有 where 的話
// 那麼這個 T 就是可以代表任意類型的泛型
// 但這裏出現了 where
// 因此 T 就表示實現了 People 和 Female 兩個 trait 的任意類型 
fn get_info<T>(p: T)
where
    T: People + Female
{
}

如果要聲明多個實現 trait 的泛型,那麼使用逗號分隔。

fn get_info<T, W>(p1: T, p2: W)
where
    T: People + Female,
    W: People + Female
{
}

可以看出,Rust 的語法表達能力還是挺豐富的。

trait 作爲返回值

trait 也是可以作爲返回值的。

struct Girl {
    name: String,
    age: i32,
    gender: String,
}

trait People {
    fn info(&self) -> String;
}

impl People for Girl {
    fn info(&self) -> String {
        format!("{} {}"&self.name, self.age)
    }
}

fn init() -> impl People {
    Girl {
        name: String::from("satori"),
        age: 16,
        gender: String::from("female"),
    }
}

fn main() {
    let g = init();
    println!("{}", g.info());  // satori 16
}

注意代碼中的變量 g,它具體是什麼類型 Rust 其實是不知道的。init 返回的不是一個具體的類型,因爲一個 trait 可以有很多種類型實現,返回任意一個都是可以的。這個時候我們應該顯式地指定變量 g 的類型,即 let g: Girl = init()。

最後 trait 也是可以帶泛型的。

struct Number(i32);

trait SomeTrait<T: PartialOrd> {
    fn compare(&self, n: T) -> bool;
}

// 爲 Number 實現 T 爲 i32 的 SomeTrait
impl SomeTrait<i32> for Number {
    fn compare(&self, n: i32) -> bool {
        self.0 >= n
    }
}

fn main() {
    let num = Number(66);
    println!("{:?}", num.compare(67));  // false
    println!("{:?}", num.compare(65));  // true
}

以上就是 trait 相關的內容,如果一個 trait 裏面沒有定義任何方法,那麼相當於不起任何約束作用,也就是空約束。

小結

本篇文章我們複習了函數、閉包、泛型和 trait,內容還是蠻多的,建議私底下也可以多動手敲一敲。

總而言之,Rust 的學習門檻確實不是一般的高,但努力總有收穫。

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