Rust 的 trait 是個啥子?

楔子

前面我們提到過 trait,那麼 trait 是啥呢?先來看個例子:

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

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

fn main() {
    let p = Point { x123 };
}

你覺得這段代碼有問題嗎?如果上一篇文章你還有印象的話,那麼會很快發現是有問題的。因爲方法 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

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

#[derive(Debug)]
struct Girl {
    nameString,
    agei32
}

// 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{nameString::from("satori"), age16};
    println!("{}", g.summary());  // name: satori, age: 16
}

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

#[derive(Debug)]
struct Girl {
    nameString,
    agei32
}

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{nameString::from("satori"), age16};
    // 雖然沒有 summary 方法,但因爲實現了 Summary 這個 trait
    // 而 trait 內部有 summary 的具體實現,所以不會報錯
    // 但如果 trait 裏面的方法只有聲明沒有實現,那麼就必須要我們手動實現了
    println!("{}", g.summary());  // hello
}

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

trait 作爲參數

到目前爲止,我們並沒有看到 trait 的實際用途,但相信你也能猜出來它是做什麼的。假設有一個函數,只要是實現了 info 方法的結構體實例,都可以作爲參數傳遞進去,這時候應該怎麼做呢?

struct Girl {
    nameString,
    agei32,
}

struct Boy {
    nameString,
    agei32,
    salaryu32,
}

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(pimpl People) -> String {
    p.info()
}

fn main() {
    let g = Girl {
        nameString::from("satori"),
        age16,
    };
    let b = Boy {
        nameString::from("可憐的我"),
        age26,
        salary3000,
    };
    // 只要實現了 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<TPeople>(pT) -> String {
    p.info()
}

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

fn get_info<TPeople>(p1T, p2T) -> String {

}
// 否則話要這麼寫
fn get_info(p1impl People, p2impl People) -> String {

}

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

struct Girl {
    nameString,
    agei32,
    genderString
}

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<TFemale>(f&T) {
    println!("{}", f.info())
}

fn main() {
    let g = Girl {
        nameString::from("satori"),
        age16,
        genderString::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(pimpl 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<TPeople + Female>(pT) {}

最後還有一個更加優雅的寫法:

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

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

fn get_info<T, W>(p1T, p2W)
where
    TPeople + Female,
    WPeople + Female
{
}

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

trait 作爲返回值

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

struct Girl {
    nameString,
    agei32,
    genderString,
}

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

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

fn init() -> impl People {
    Girl {
        nameString::from("satori"),
        age16,
        genderString::from("female"),
    }
}

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

一個 trait 可以有很多種類型實現,返回任意一個都是可以的。

實現一個 max 函數

這裏我們定義一個函數 max,返回數組裏面的最大元素,這裏先假定數組是 i32 類型。

// arr 接收一個數組,我們將它聲明爲 &[i32]
// 這個聲明比較特殊,我們舉幾個例子解釋一下
// arr: [i32;5],表示接收類型爲 i32 長度爲 5 的靜態數組
// arr: Vec<f64>,表示接收類型爲 f64 的動態數組,長度不限
/* arr: &[i32],表示接收 i32 類型數組的引用
   並且數組可以是動態數組,也可以是靜態數組,長度不限
   對於當前求最大值來說,我們不應該關注數組是靜態的還是動態的
   所以應該聲明爲 &[i32],表示都支持
*/
fn max(arr&[i32]) -> i32{
    if arr.len() == 0 {
        panic!("數組爲空")
    }
    // 獲取數組的第一個元素,然後和後續元素依次比較
    let mut largest = arr[0];
    for &item in arr {
        if largest < item {
            largest = item
        }
    }
    largest
}

fn main() {
    let largest = max(&vec![1, 23, 13, 4, 15]);
    println!("{}", largest);  // 23
}

還是很簡單的,但問題來了,如果我希望它除了支持整型數組外,還支持浮點型該怎麼辦呢?難道再定義一個函數嗎?顯然這是不現實的,於是我們可以考慮泛型。

fn max<T>(arr&[T]) -> T {
    if arr.len() == 0 {
        panic!("數組爲空")
    }
    let mut largest = arr[0];
    for &item in arr {
        if largest < item {
            largest = item
        }
    }
    largest
}

使用泛型的話,代碼就是上面這個樣子,你覺得代碼有問題嗎?

不用想,問題大了去了。首先函數接收的是數組的引用,那麼函數調用結束後,數組依舊保持有效,那麼數組裏面的元素顯然也是有效的。但在給 largest 賦值的時候,等號右邊是 arr[0]。如果數組裏面的元素不是可 Copy 的,那麼就會失去所有權,因爲 Rust 不會拷貝堆數據,那這樣的話數組之後就不能用了。所以這種情況 Rust 要求元素是可 Copy 的,但實際情況是不是呢?Rust 是不知道的,所以會報錯,認爲不是可 Copy 的,這是第一個錯誤。

然後是 for &item in arr,這段代碼的錯誤和上面相同,在遍歷的時候會依次將元素拷貝一份賦值給 item。但要求拷貝之後彼此互不影響,這就意味着數據必須全部在棧上。但 T 代表啥類型,該類型的數據是否全部在棧上 Rust 是不知道的,於是報錯。

第三個錯誤就是 largest < item,因爲這涉及到了比較,但 T 類型的數據能否比較呢?Rust 也是不知道的,所以報錯。

因此基於以上原因,如果想讓上述代碼成立,那麼必須對 T 進行一個限制。

fn max<T>(arr&[T]) -> T
where
    // 相當於告訴 Rust
    // 這個 T 是可比較的、可 Copy 的
    // 或者說 T 實現了 PartialOrd 和 Copy 這兩個 trait
    TPartialOrd + Copy,
{
    if arr.len() == 0 {
        panic!("數組爲空")
    }
    let mut largest = arr[0];
    for &item in arr {
        if largest < item {
            largest = item
        }
    }
    largest
}

fn main() {
    let largest = max(&vec![1, 23, 13, 4, 15]);
    println!("{}", largest); // 23
    let largest = max(&vec![1.1, 23.1, 13.1, 4.1, 15.1]);
    println!("{}", largest); // 23.1
}

以上我們就實現了數組求最大值的邏輯,通過對 T 進行限制,告訴 Rust 泛型 T 代表的類型實現了 PartialOrd 和 Copy 這兩個 trait。然後當我們調用的時候,Rust 就會檢測類型是否合法:

顯然當元素類型爲 String 的時候就會報錯,因爲 Rust 檢測到該類型沒有實現 Copy 這個 trait。

那如果我希望,max 函數也支持 String 類型的數組呢?

fn max<T>(arr&[T]) -> &T
where
    // T 可以不實現 Copy trait
    // 但必須實現 PartialOrd
    TPartialOrd,
{
    if arr.len() == 0 {
        panic!("數組爲空")
    }
    // 這裏必須要拿到引用,可能有人覺得調用 clone 可不可以
    // 答案是不可以,因爲這個函數不僅支持 String
    // 還要支持整型、浮點型,所以只能獲取引用
    let mut largest = &arr[0];
    // 因爲 arr 是個引用,所以遍歷出來的 item 也是元素的引用
    for item in arr {
        // 雖然這裏表面上比較的是引用,但其實比較的是值
        // 比如 let (a, b) = (11, 22)
        // 那麼 a < b 和 &a < &b 的結果是一樣的
        if largest < item {
            largest = item
        }
    }
    largest
}

fn main() {
    let arr = &vec![String::from("A"), String::from("Z")];
    println!("{}", max(arr)); // Z

    let arr = &vec![1, 22, 11, 34, 19];
    println!("{}", max(arr)); // 34

    let arr = &vec![1.1, 22.1, 11.2, 34.3, 19.8];
    println!("{}", max(arr)); // 34.3
}

此時我們就實現了基礎類型的比較,還是需要好好理解一下的。

關於 Rust 的 trait 我們就說到這裏,下一篇文章來聊一聊生命週期。

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