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。例如:
-
如果結構體中的所有字段類型都是 Send,那麼編譯器會自動將該結構體標記爲 Send,而不需要手動輸入。
-
如果結構體的一個字段實現了 Clone,而另一個字段沒有,那麼結構體就不能再派生 Clone 了,但是可以通過在 Arc 或 Rc 中包裝相關類型來解決這個問題。
爲什麼標記 Trait 在 Rust 中很重要?
Rust 中的標記特徵構成了生態系統的核心,並允許我們提供在其他語言中無法實現的保證。以 Send 類型爲例,我們可以確保跨線程發送類型總是安全的。這使得併發問題更容易處理。標記 Trait 也會影響其他事情:
-
Copy Trait 需要通過執行逐位複製來複制內容 (儘管這需要 Clone)。嘗試按位複製指針只返回地址!這也是 String 不能被複制而必須被克隆的原因:Rust 中的 String 是智能指針。
-
Pin Trait 允許我們將一個值 “固定” 到內存中的一個靜態位置
-
Sized trait 允許我們在編譯時將類型定義爲具有固定大小的尺寸——這已經在大多數類型中自動實現了
還有一些標記 Trait,比如?Sized、!Send 和! Sync。與 size, Send 和 Sync 相比,它們是相反的 Trait 邊界,做完全相反的事情:
-
?Sized:動態調整類型大小
-
!Send:告訴編譯器一個對象絕對不能發送給其他線程
-
!Sync:告訴編譯器對象的引用絕對不能在線程之間共享
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 的安全性。安全性需要滿足的條件包括:
-
類型不能是 Self: Sized
-
類型必須在函數參數中使用某種類型的 “self”(無論是 & self, self, mut self 等)
-
類型不能返回 Self
這源於這樣一個事實:通過將分派移到運行時,編譯器無法猜測類型的大小——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