Rust:Trait 和泛型的高級用法

在這篇文章中,我們將快速複習一下 Rust 的 trait 和泛型,以及實現一些更高級的 trait 邊界和類型簽名。

快速回顧一下 Rust 的 Trait

編寫一個 Rust trait,如下:

pub trait MyTrait {
    fn some_method(&self) -> String;
}

我們定義一個 Struct 類型來實現 MyTrait:

struct MyStruct;

impl MyTrait for MyStruct {
    fn some_method(&self) -> String {
        "Hi from some_method!".to_string()
    }
}

也可以在我們自有的類型上實現我們不擁有的 Trait,或者在不擁有的類型上實現我們擁有的 Trait——但不能兩者都實現!不能這麼做的原因是因爲 Trait 一致性。要確保我們不會意外地有衝突的 trait:

// 在MyStruct上實現Into<T>,一個我們不擁有的trait
impl Into<String> for MyStruct {
    fn into(self) -> String {
        "Hello world!".to_string()
    }
}

// 爲一個我們不擁有的類型實現MyTrait
impl MyTrait for String {
    fn some_method(&self) -> String {
        self.to_owned()
    }
}

// 這樣是錯誤的
impl Into<String> for &str {
    fn into(self) -> String {
        self.to_owned()
    }
}

一種常見的解決方法是創建一個新類型模式——也就是說,用一個單字段元組結構體來封裝我們想要擴展的類型。

struct MyStr<'a>(&'a str);

// 注意,實現From<T>也實現了Into<T>
impl<'a> From<MyStr<'a>> for String {
    fn from(string: MyStr<'a>) -> String {
        string.0.to_owned()
    }
}

fn main() {
    let my_str = MyStr("Hello world!");
    let my_string: String = my_str.into();

    println!("{my_string}");
}

如果多個 trait 有相同的方法名,需要手動聲明從哪個 trait 實現中調用方法:

pub trait MyTrait {
    fn some_method(&self) -> String;
}

pub trait MyTraitTwo {
    fn some_method(&self) -> i32;
}

struct MyStruct;

impl MyTrait for MyStruct {
    fn some_method(&self) -> String {
        "Hi from some_method!".to_string()
    }
}

impl MyTraitTwo for MyStruct {
    fn some_method(&self) -> i32 {
        42
    }
}

fn main() {
    let my_struct = MyStruct;
    println!("{}", MyTraitTwo::some_method(&my_struct));
}

有時,可能希望 Trait 中的方法能夠擁有默認實現,我們可以通過簡單地在 trait 中定義方法來實現這一點。

trait MyTrait {
    fn some_method(&self) -> String {
        "Boo!".to_string()
    }
}

Trait 也可以用其他 Trait 作爲邊界限制,以 std::error:: error 特性爲例:

trait Error: Debug + Display {
    // .. 如果需要,可以重新實現這裏提供的方法
}

這裏,我們顯式地告訴編譯器,我們的類型必須在實現 Error 之前實現 Debug 和 Display 兩個 trait。

標記 Trait

標記 Trait 被用作 “標記”,當爲一個類型實現標記 Trait 時,可以維持某些特性。它們沒有方法或特定的屬性,通常用於確保編譯器的某些行爲。

特別是這兩個標記 Trait,對我們來說非常重要:Send 和 Sync。手動實現 Send 和 Sync 是不安全的——這是因爲需要手動確保其安全實現,Unpin 也是另一個例子。

除此之外,標記 Trait(一般來說) 也是自動實現的。如果一個結構體的字段都實現了 trait,那麼這個結構體本身也會實現 trait。例如:

爲什麼標記 Trait 在 Rust 中很重要?

Rust 中的標記特徵構成了生態系統的核心,並允許我們提供在其他語言中無法實現的保證。以 Send 類型爲例,我們可以確保跨線程發送類型總是安全的。這使得併發問題更容易處理。標記 Trait 也會影響其他事情:

還有一些標記 Trait,比如?Sized、!Send 和! Sync。與 size, Send 和 Sync 相比,它們是相反的 Trait 邊界,做完全相反的事情:

Trait Object 和動態調度

動態分派本質上是在運行時選擇使用多態函數的哪個實現的過程。雖然 Rust 出於性能原因傾向於靜態分派,但通過 trait object 使用動態分派也有好處。

使用 trait object 最常見的模式是 Box,我們需要將 trait object 包裝在 Box 中,以使其實現 Sized 的 trait。因爲我們將多態過程移到了運行時,編譯器無法知道類型的大小。將類型包裝在指針中 (或將其“裝箱”) 將其放在堆中而不是棧中。

struct MyStruct {
     my_field: Box<dyn MyTrait>
}

// this works!
fn my_function(my_item: Box<dyn MyTrait>) {
     // .. some code here
}

// this doesn't!
fn my_function(my_item: dyn MyTrait) {
     // .. some code here
}

trait MySizedTrait: Sized {
    fn some_method(&self) -> String {
        "Boo!".to_string()
    }
}

// 由於大小限制而無法編譯的非法結構體
struct MyStruct {
    my_field: Box<dyn MySizedTrait>
}

動態分派的主要優點是你的函數不需要知道具體的類型,只要類型實現了 trait,你就可以把它作爲 trait object 使用。從用戶的角度來看,編譯器並不關心底層的具體類型是什麼——只關心它實現了 trait。

缺點是需要確保 trait object 的安全性。安全性需要滿足的條件包括:

這源於這樣一個事實:通過將分派移到運行時,編譯器無法猜測類型的大小——Trait Object 在編譯時沒有固定的大小。這也是爲什麼我們需要像前面提到的那樣將動態分派的對象封裝在 Box 中,並將它們放在堆上。因此,應用程序的性能也會受到影響——當然,這取決於正在使用多少動態分派的對象以及它們有多大。

結合 Trait 和泛型

我們可以毫不費力地編寫這樣一個實現泛型的結構體:

struct MyStruct<T> {
    my_field: T
}

然而,爲了能夠將我們的結構體與來自其他容器的類型一起使用,我們需要確保我們的結構體能夠保證某些行爲。這就是我們添加 trait 邊界的地方:一個類型必須滿足條件才能使其編譯。你可能會發現一個常見的特徵綁定是 Send + Sync + Clone:

struct MyStruct<T: Send + Sync + Clone> {
    my_field: T
}

作爲一個使用泛型 Trait 更復雜的例子,你可能偶爾需要爲自己的類型重新實現,以 Axum 的 FromRequest trait 爲例:

use axum::extract::State;
use axum::response::IntoResponse;

trait FromRequest<S>
   where S: State
    {
    type Rejection: IntoResponse;

    fn from_request(r: Request, _state: S) -> Result<Self, Self::Rejection>;
}

這裏我們還可以通過使用 where 子句來添加 trait 邊界。這個特性只是告訴我們 S 實現了 State。但是,State 還要求內部對象爲 Clone。通過使用複雜的 trait 邊界,我們可以創建大量使用 trait 的框架系統,從而能夠實現一些人所說的 “trait 魔法”。看一下這個 trait 邊界的例子:

use std::future::Future;

struct MyStruct<T, B> where
   B: Future<Output = String>,
   T: Fn() -> B
{
    my_field: T
}

#[tokio::main]
async fn main() {
    let my_struct = MyStruct { my_field: hello_world };
    let my_future = (my_struct.my_field)();
    println!("{:?}", my_future.await);
}

async fn hello_world() -> String {
    "Hello world!".to_string()
}

上面的單字段結構體存儲了一個函數閉包,該閉包返回 impl Future<Output = String>,我們將 hello_world 存儲在其中,然後在主函數中調用它。

像這樣結合 Trait 和泛型是非常強大的,有效利用這一點的一個用例是 HTTP 框架。例如,Actix Web 有一個名爲 Handler 的 trait,它接受許多參數,調用自己,然後有一個名爲 call 的函數產生一個 Future:

pub trait Handler<Args>: Clone + 'static {
     type Output;
     type Future: Future<Output = Self::Output>;

     fn call(&self, args: Args) -> Self::Future;
}

這樣我們就可以將這個 Trait 擴展爲一個處理程序函數。我們可以告訴 web 服務,我們有一個函數,它有一個內部函數,一些參數和 Responder (Actix web 的 HTTP 響應 Trait):

pub fn to<F, Args>(handler: F) -> Route where
    F: Handler<Args>,
    Args: FromRequest + 'static,
    F::Output: Responder + 'static 
{
    // .. the actual function  code here
}

其他框架 (如 Axum) 也遵循相同的方法來提供符合人體工程學的開發人員體驗。

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