午夜视频在线网站,日韩视频精品在线,中文字幕精品一区二区三区在线,在线播放精品,1024你懂我懂的旧版人,欧美日韩一级黄色片,一区二区三区在线观看视频

分享

Rust 的 trait 是個(gè)啥子?

 古明地覺O_o 2022-12-08 發(fā)布于北京

楔子

前面我們提到過 trait,那么 trait 是啥呢?先來看個(gè)例子:

#[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 };
}

你覺得這段代碼有問題嗎?如果上一篇文章你還有印象的話,那么會(huì)很快發(fā)現(xiàn)是有問題的。因?yàn)榉椒?m 的第一個(gè)參數(shù)是引用,這就意味著方法調(diào)用完畢之后,結(jié)構(gòu)體實(shí)例依舊保持有效,也意味著實(shí)例的所有成員值都保持有效。

但在方法 m 里面,我們將成員 x 的值賦給了變量 var。如果成員 x 的類型不是可 Copy 的,也就是數(shù)據(jù)不全在棧上,還涉及到堆,那么就會(huì)轉(zhuǎn)移所有權(quán),因?yàn)?Rust 默認(rèn)不會(huì)拷貝堆數(shù)據(jù)。所以調(diào)用完方法 m 之后,成員的值不再有效,進(jìn)而使得結(jié)構(gòu)體不再有效。

所以 Rust 為了避免這一點(diǎn),在賦值的時(shí)候強(qiáng)制要求 self.x 的類型必須是可 Copy 的,但泛型 T 可以代表任意類型,它不滿足這一特性?;蛘哒f T 最終代表的類型是不是可 Copy 的,Rust 是不知道的,所以 Rust 干脆認(rèn)為它不是可 Copy 的。

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


什么是 trait

trait 類似于 Go 里面的接口,相當(dāng)于告訴編譯器,某種類型具有哪些可以與其它類型共享的功能。

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

// trait 類似 Go 里面的接口
// 然后里面可以定義一系列的方法
// 這里我們創(chuàng)建了一個(gè)名為 Summary 的 trait
// 并在內(nèi)部定義了一個(gè) summary 方法
trait Summary {
    // trait 里面的方法只需要寫聲明即可
    fn summary(&self) -> String;
}

// Go 里面只要實(shí)現(xiàn)了接口里面的方法,便實(shí)現(xiàn)了該接口
// 但是在 Rust 里面必須顯式地指明實(shí)現(xiàn)了哪一個(gè) trait
// impl Summary for Girl 表示為類型 Girl 實(shí)現(xiàn) Summary 這個(gè) trait
impl Summary for Girl {
    fn summary(&self) -> String {
        // format! 宏用于拼接字符串,它的語法和 println! 一樣
        // 并且這兩個(gè)宏都不會(huì)獲取參數(shù)的所有權(quán)
        // 比如這里的 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 里面的方法只需要寫上聲明即可,實(shí)現(xiàn)交給具體的結(jié)構(gòu)體來做。當(dāng)然啦,trait 里面的方法也是可以有默認(rèn)實(shí)現(xiàn)的。

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

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

impl Summary for Girl {
    // 如果要為類型實(shí)現(xiàn) trait,那么要實(shí)現(xiàn) trait 里面所有的方法
    // 這一點(diǎn)和 Go 的接口是相似的,但 Go 里面實(shí)現(xiàn)接口是隱式的
    // 只要你實(shí)現(xiàn)了某個(gè)接口所有的方法,那么默認(rèn)就實(shí)現(xiàn)了該接口
    // 但在 Rust 里面,必須要顯式地指定實(shí)現(xiàn)了哪個(gè) trait
    // 同時(shí)還要實(shí)現(xiàn)該 trait 里的所有方法

    // 但 Rust 的 trait 有一點(diǎn)特殊,Go 接口里面的方法只能是定義
    // 而 trait 里面除了定義之外,也可以有具體的實(shí)現(xiàn)
    // 如果 trait 內(nèi)部已經(jīng)實(shí)現(xiàn)了,那么這里就可以不用實(shí)現(xiàn)
    // 不實(shí)現(xiàn)的話則用 trait 的默認(rèn)實(shí)現(xiàn),實(shí)現(xiàn)了則調(diào)用我們實(shí)現(xiàn)的

    // 因此這里不需要定義任何的方法,它依舊實(shí)現(xiàn)了 Summary 這個(gè) trait
    // 只是我們?nèi)匀灰ㄟ^ impl Summary for Girl 顯式地告訴 Rust
    // 如果只寫 impl Girl,那么 Rust 則不認(rèn)為我們實(shí)現(xiàn)了該 trait
}
fn main() {
    let g = Girl{name: String::from("satori"), age: 16};
    // 雖然沒有 summary 方法,但因?yàn)閷?shí)現(xiàn)了 Summary 這個(gè) trait
    // 而 trait 內(nèi)部有 summary 的具體實(shí)現(xiàn),所以不會(huì)報(bào)錯(cuò)
    // 但如果 trait 里面的方法只有聲明沒有實(shí)現(xiàn),那么就必須要我們手動(dòng)實(shí)現(xiàn)了
    println!("{}", g.summary());  // hello
}

總結(jié)一下就是 trait 里面可以有很多的方法,這個(gè)方法可以只有聲明,也可以同時(shí)包含實(shí)現(xiàn)。如果要為類型實(shí)現(xiàn)某個(gè) trait,那么要通過 impl xxx for 進(jìn)行指定,并且實(shí)現(xiàn)該 trait 內(nèi)部定義的所有方法。但如果 trait 的某個(gè)方法已經(jīng)包含了具體實(shí)現(xiàn),那么我們也可以不實(shí)現(xiàn),會(huì)使用 trait 的默認(rèn)實(shí)現(xiàn)。


trait 作為參數(shù)

到目前為止,我們并沒有看到 trait 的實(shí)際用途,但相信你也能猜出來它是做什么的。假設(shè)有一個(gè)函數(shù),只要是實(shí)現(xiàn)了 info 方法的結(jié)構(gòu)體實(shí)例,都可以作為參數(shù)傳遞進(jìn)去,這時(shí)候應(yīng)該怎么做呢?

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

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

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

// 為 Girl 和 Boy 實(shí)現(xiàn) People 這個(gè) 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)
    }
}

// 定義一個(gè)函數(shù),注意參數(shù) p 的類型
// 如果是 p: xxx,則表示參數(shù) p 的類型為 xxx
// 如果是 p: impl xxx,則表示參數(shù) p 的類型任意,只要實(shí)現(xiàn)了xxx這個(gè)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,
    };
    // 只要實(shí)現(xiàn)了 People 這個(gè) trait
    // 那么實(shí)例都可以作為參數(shù)傳遞給 get_info
    println!("{}", get_info(g)); // satori 16
    println!("{}", get_info(b)); // 可憐的我 26 3000
}

然后以 trait 作為參數(shù)的時(shí)候,還有另外一種寫法:

// 如果是 <T> 的話,那么 T 表示泛型,可以代表任意類型
// 但這里是 <T: People>,那么就不能表示任意類型了
// 它表示的應(yīng)該是實(shí)現(xiàn)了 People 這個(gè) trait 的任意類型
fn get_info<T: People>(p: T) -> String {
    p.info()
}

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

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

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

}

當(dāng)然啦,一個(gè)類型并不僅僅可以實(shí)現(xiàn)一個(gè) trait,而是可以實(shí)現(xiàn)任意多個(gè) trait。

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

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

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

// 不同的 trait 內(nèi)部可以有相同的方法
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 前面加上了一個(gè) &
// 表示調(diào)用的時(shí)候傳遞的是引用
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 內(nèi)部的方法可以相同也可以不同,而 Girl 同時(shí)實(shí)現(xiàn)了 People 和 Female 兩個(gè) trait,所以它可以傳遞給 get_info1,也可以傳遞給 get_info2。然后為 trait 實(shí)現(xiàn)了哪個(gè)方法,就調(diào)用哪個(gè)方法,所以兩者的打印結(jié)果不一樣。

那么問題來了,如果我在定義函數(shù)的時(shí)候,要求某個(gè)參數(shù)同時(shí)實(shí)現(xiàn)以上兩個(gè) trait,該怎么做呢?

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

// 如果想接收引用的話,那么需要這么聲明
// 因?yàn)閮?yōu)先級(jí)的原因,需要將 impl People + Female 整體括起來
fn get_info2(p: &(impl People + Female)) {}

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

最后還有一個(gè)更加優(yōu)雅的寫法:

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

如果要聲明多個(gè)實(shí)現(xiàn) trait 的類型,那么使用逗號(hào)分隔。

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

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


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è) trait 可以有很多種類型實(shí)現(xiàn),返回任意一個(gè)都是可以的。


實(shí)現(xiàn)一個(gè) max 函數(shù)

這里我們定義一個(gè)函數(shù) max,返回?cái)?shù)組里面的最大元素,這里先假定數(shù)組是 i32 類型。

// arr 接收一個(gè)數(shù)組,我們將它聲明為 &[i32]
// 這個(gè)聲明比較特殊,我們舉幾個(gè)例子解釋一下
// arr: [i32;5],表示接收類型為 i32 長度為 5 的靜態(tài)數(shù)組
// arr: Vec<f64>,表示接收類型為 f64 的動(dòng)態(tài)數(shù)組,長度不限
/* arr: &[i32],表示接收 i32 類型數(shù)組的引用
   并且數(shù)組可以是動(dòng)態(tài)數(shù)組,也可以是靜態(tài)數(shù)組,長度不限
   對(duì)于當(dāng)前求最大值來說,我們不應(yīng)該關(guān)注數(shù)組是靜態(tài)的還是動(dòng)態(tài)的
   所以應(yīng)該聲明為 &[i32],表示都支持
*/

fn max(arr: &[i32]) -> i32{
    if arr.len() == 0 {
        panic!("數(shù)組為空")
    }
    // 獲取數(shù)組的第一個(gè)元素,然后和后續(xù)元素依次比較
    let mut largest = arr[0];
    for &item in arr {
        if largest < item {
            largest = item
        }
    }
    largest
}

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

還是很簡單的,但問題來了,如果我希望它除了支持整型數(shù)組外,還支持浮點(diǎn)型該怎么辦呢?難道再定義一個(gè)函數(shù)嗎?顯然這是不現(xiàn)實(shí)的,于是我們可以考慮泛型。

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

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

不用想,問題大了去了。首先函數(shù)接收的是數(shù)組的引用,那么函數(shù)調(diào)用結(jié)束后,數(shù)組依舊保持有效,那么數(shù)組里面的元素顯然也是有效的。但在給 largest 賦值的時(shí)候,等號(hào)右邊是 arr[0]。如果數(shù)組里面的元素不是可 Copy 的,那么就會(huì)失去所有權(quán),因?yàn)?Rust 不會(huì)拷貝堆數(shù)據(jù),那這樣的話數(shù)組之后就不能用了。所以這種情況 Rust 要求元素是可 Copy 的,但實(shí)際情況是不是呢?Rust 是不知道的,所以會(huì)報(bào)錯(cuò),認(rèn)為不是可 Copy 的,這是第一個(gè)錯(cuò)誤。

然后是 for &item in arr,這段代碼的錯(cuò)誤和上面相同,在遍歷的時(shí)候會(huì)依次將元素拷貝一份賦值給 item。但要求拷貝之后彼此互不影響,這就意味著數(shù)據(jù)必須全部在棧上。但 T 代表啥類型,該類型的數(shù)據(jù)是否全部在棧上 Rust 是不知道的,于是報(bào)錯(cuò)。

第三個(gè)錯(cuò)誤就是 largest < item,因?yàn)檫@涉及到了比較,但 T 類型的數(shù)據(jù)能否比較呢?Rust 也是不知道的,所以報(bào)錯(cuò)。

因此基于以上原因,如果想讓上述代碼成立,那么必須對(duì) T 進(jìn)行一個(gè)限制。

fn max<T>(arr: &[T]) -> T
where
    // 相當(dāng)于告訴 Rust
    // 這個(gè) T 是可比較的、可 Copy 的
    // 或者說 T 實(shí)現(xiàn)了 PartialOrd 和 Copy 這兩個(gè) trait
    T: PartialOrd + Copy,
{
    if arr.len() == 0 {
        panic!("數(shù)組為空")
    }
    let mut largest = arr[0];
    for &item in arr {
        if largest < item {
            largest = item
        }
    }
    largest
}

fn main() {
    let largest = max(&vec![12313415]);
    println!("{}", largest); // 23
    let largest = max(&vec![1.123.113.14.115.1]);
    println!("{}", largest); // 23.1
}

以上我們就實(shí)現(xiàn)了數(shù)組求最大值的邏輯,通過對(duì) T 進(jìn)行限制,告訴 Rust 泛型 T 代表的類型實(shí)現(xiàn)了 PartialOrd 和 Copy 這兩個(gè) trait。然后當(dāng)我們調(diào)用的時(shí)候,Rust 就會(huì)檢測類型是否合法:

顯然當(dāng)元素類型為 String 的時(shí)候就會(huì)報(bào)錯(cuò),因?yàn)?Rust 檢測到該類型沒有實(shí)現(xiàn) Copy 這個(gè) trait。

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

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

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

    let arr = &vec![122113419];
    println!("{}", max(arr)); // 34

    let arr = &vec![1.122.111.234.319.8];
    println!("{}", max(arr)); // 34.3
}

此時(shí)我們就實(shí)現(xiàn)了基礎(chǔ)類型的比較,還是需要好好理解一下的。

關(guān)于 Rust 的 trait 我們就說到這里,下一篇文章來聊一聊生命周期。

    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多