聊聊 rust trait

學 Rust 的一定離不開 trait, 告訴編譯器某些類型擁有的,且能夠被其他類型共享的功能,官方的定義叫做 Defining Shared Behavior 共享行爲。同時還可以對泛型參數進行約束,將其指定爲某些特定行爲的類型。讀過 你真的瞭解泛型嘛 朋友肯定知道,rust 的 trait 和 go interface 非常像,但是遠比後者強大

Rust Ownership 三原則 開篇講到對像在離開作用域時,會調用 drop 方法析構,這就是一個 drop trait

trait 簡介

定義

Summary 官方示例特徵

pub trait Summary {
    fn summarize(&self) -> String;
}

pub 關鍵字表示能被其它模塊調用,如果是私有的可以省略。Summary trait 擁有一個函數 summarize, 只需要描述函數簽名即可,不需要寫上函數體

type Endpoint interface {
 // SetClient sets the http.Client to use.
 SetClient(client *http.Client)
}

可以看到和 go interface 的定義很像,只不過 go 的 self 是隱式傳遞的,而 rust 需要顯示傳遞,並且只能是 &self 引用。爲什麼一定要用引用呢?這是就鏽兒們吐糟和難以理解的點:如果傳入 self 那麼函數就會擁有變量所有權,離開函數後就會被析構 drop 掉

實現

官網以 NewsArticleTwitter 爲例子,展示如何實現 Summary trait

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

impl 關鍵字,後面是要實現的 trait 名稱,for 後面是結構體名

fn main() {
    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    };

    println!("1 new tweet: {}", tweet.summarize());
}

調用的話也比較簡單,初始化 tweet 對像後,調用相應的方法 summarize 即可。這裏可以看到和 go interface 有些區別,go 不需要顯示的指定實現了某些接口,而 rust 要顯示聲明

做爲入參

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
 f(w, r)
}

上面是 go net/httpServeHTTP 函數,入參 ResponseWriter 就是一個接口。其實 rust 也常這麼用

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

這裏傳入參數 item 類型是 impl Summary 的引用,如果不傳引用,所有權又被轉移到 notify 函數里

做爲出參

fn returns_summarizable() -> impl Summary {
    Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    }
}

同樣,返回值時,指定類型是 impl Summary, 與 go 不一樣的是,函數體內不允許返回不同類型,儘管他們都實現了 Summary

fn returns_summarizable(switch: bool) -> impl Summary {
    if switch {
        NewsArticle {
        ......
        }
    } else {
        Tweet {
        .....
        }
    }
}

比如這種 if-else 編譯會報錯

   | |_|_________^ expected struct `NewsArticle`, found struct `Tweet`
53 |   |     }
   |   |_____- `if` and `else` have incompatible types
   |
help: you could change the return type to be a boxed trait object
   |
31 | fn returns_summarizable(switch: bool) -> Box<dyn Summary> {
   |                                          ^^^^^^^        ^
help: if you change the return type to expect trait objects, box the returned expressions

rust 編譯器很智能,提示我們要用 Box<dyn Summary>, 那麼最終的實現如下

fn returns_summarizable(switch: bool) -> Box<dyn Summary> {
    if switch {
        let na  = NewsArticle {
          ......
        };
        Box::new(na)
    } else {
        let t = Tweet {
          ......
        };
        Box::new(t)
    }
}

trait 與泛型

fn largest<T: PartialOrd + Copy>(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 result = largest(&number_list);
    println!("The largest number is {}", result);

    let char_list = vec!['y''m''a''q'];

    let result = largest(&char_list);
    println!("The largest char is {}", result);
}

官網給了例子 largest 泛型函數,傳入類型 T, 約束是類型必須實現 PartialOrdCopy 特徵,用於比較運算和 copy 實現

fn largest<T>(list: &[T]) -> T
    where T: PartialOrd + Copy

爲了簡潔,可以使用 where 關鍵詞。那麼問題來了什麼時候使用泛型,什麼時候使用 trait 呢?

fn largest<T>(left: T, right: T) -> T

這是泛型函數,由於單態化,傳參 left, right 類型都是 T 然後返回 T 類型,要求類型必須一樣。不可能傳入 i32 返回 i8. 除非泛型簽名是 (left: T, right: U)

pub trait Draw {
    fn draw(&self);
}

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}

impl Screen {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

例子模擬了 GUI 的屏幕實現,只要實現了 Draw 特徵的組件就可以,可以渲染方形,渲染圓形,也可以是三角形

高級特性

默認實現方法

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

與 go 不同,rust trait 可以擁有默認實現,這樣結構體 impl 時無需實現該方法

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

同時,Summary 的默認實現方法,也可以調用其它 trait 方法。如上例所示,Tweet 只需實現 summarize_author 即可

有條件的實現方法

參考 Using Trait Bounds to Conditionally Implement Methods 章節

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

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

泛型結構體 Pair 擁有兩個字段,x, y 類型都是 T. 我們可以針對部份類型實現某些方法,這在 rust 裏大量應用 (rust 讓人難懂的就是柔和了泛型,生命週期與所有權)

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}

此時只有實現了 DisplayPartialOrd 的類型,才能調用 cmd_display 方法

blanket implementations

我們同樣可以爲實現了某個 trait 的類型有條件的實現另外一個 trait, rust 標準庫裏也大量應用這個方法。對於滿足 trait 約束的所有類型實現 trait 也被稱作 blanket implementations, 有的翻譯成覆蓋實現,有的叫一籃子實現

impl<T: Display> ToString for T {
    // --snip--
}

比如標準庫裏,給所有實現了 Display 的類型,都實現了 ToString trait

let s = 3.to_string();

然後就可以直接用 to_string 將整型轉成字符串

繼承

trait 同樣是可以繼承的

trait Vehicle {
  fn get_price(&self) -> u64;
}

trait Car: Vehicle {
  fn model(&self) -> String;
}

trait Car 實現依賴於 Vehicle, 也就是說,任何想實現 Car 的必須也同時要實現 Vehicle. 可以參考 Fn, FnOnce, FnMut 傻傻分不清

Marker trait

Rust 裏有大量的 Marker trait, 不包含任何方法,被稱爲標記特徵,用於告訴編譯器這些類型實現了某些特徵,得到編譯期的某些保證。比如 Sync, Send, Copy 特徵

還有需要用到的 Sized, ?Sized 等等

泛型 trait

pub trait From<T> {
  fn from(T) -> Self;
}

該例子來自 From<T>Into<T> 特徵,允許從某種類型,轉換爲類型 T,反之同樣

Associated Types

參考官網 Associated Types, 上面提到了泛型特徵,但是實際應用中不夠方便,關聯類型是更好的選擇

trait 實現者需要根據自己特定場景來爲聯聯類型指定具體的類型,通過這一技術,我們可以定義出包含某些類型的 trait, 需無須在實現前確定它們的具體類型。來看一個例子

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

Item 是佔位符,next 函數表明它返回類型是 Option<Self::Item>, 實現者只需要爲 Item 指定具體的類型

struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        // --snip--
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

看起來並沒有什麼特殊之處,如果我們用上面提到的泛型 trait 定義會什麼樣呢?

pub trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}

這是一個泛型 trait, 我需要爲 Counter 指定 u32 的實現。對於其它的迭代器,需要指定 String 實現,等等。每次使用時,也要標註類型,非常麻煩,如果用關聯類型,只需要指定一次

pub trait Add<Rhs = Self> {
    /// The resulting type after applying the `+` operator.
    type Output;
    fn add(self, rhs: Rhs) -> Self::Output;
}

struct Complex<T> {
    real: T,
    imag: T,
}

impl<T> Add for Complex<T>
    where T: Add<Output=T>   // or, `where T: Add<T, Output=T>`
{
    type Output = Complex<T>;

    fn add(self, rhs: Self) -> Self::Output {
        Complex {
            real: self.real + rhs.real,
            imag: self.imag + rhs.imag,
        }
    }
}

這是運符符重載的例子,網上引用的比較多,大家可以仔細看下實現

小結

本文先分享這些,下一篇再分享更多 rust trait 內容。寫文章不容易,如果對大家有所幫助和啓發,請大家幫忙點擊在看點贊分享 三連

關於 trait 大家有什麼看法,歡迎留言一起討論,如果理解有誤,請大家指正 ^_^

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