?? 歡迎大家來到景天科技苑??
🎈🎈 養成好習慣,先贊后看哦~🎈🎈
🏆 作者簡介:景天科技苑
🏆《頭銜》:大廠架構師,華為云開發者社區專家博主,阿里云開發者社區專家博主,CSDN全棧領域優質創作者,掘金優秀博主,51CTO博客專家等。
🏆《博客》:Rust開發,Python全棧,Golang開發,云原生開發,PyQt5和Tkinter桌面開發,小程序開發,人工智能,js逆向,App逆向,網絡系統安全,數據分析,Django,fastapi,flask等框架,云原生K8S,linux,shell腳本等實操經驗,網站搭建,數據庫等分享。所屬的專欄:Rust語言通關之路
景天的主頁:景天科技苑
文章目錄
- Rust trait高級用法
- 一、Trait基礎回顧
- 1.1 Trait定義與實現
- 1.2 Trait作為參數
- 1.3 Trait作為返回類型
- 二、關聯類型(Associated Types)
- 三、默認泛型類型參數和運算符重載
- 四、完全限定語法與消歧義:調用相同名稱的方法
- 五、父 trait 用于在另一個 trait 中使用某 trait 的功能
- 六、newtype 模式用于在外部類型上實現外部 trait
Rust trait高級用法
Rust語言中的trait是其類型系統的核心特性之一,它提供了定義共享行為的強大機制。
對于初學者來說,trait類似于其他語言中的"接口",但實際上Rust的trait功能要強大得多。
本文將深入探討trait的高級用法,通過實際案例展示如何充分利用這一特性來編寫靈活、可重用且類型安全的Rust代碼。
一、Trait基礎回顧
在深入高級用法之前,讓我們先簡要回顧trait的基本概念。
1.1 Trait定義與實現
// 定義一個簡單的trait
trait Greet {fn greet(&self) -> String;
}// 為具體類型實現trait
struct Person {name: String,
}impl Greet for Person {fn greet(&self) -> String {format!("Hello, my name is {}", self.name)}
}
1.2 Trait作為參數
fn print_greeting<T: Greet>(item: T) {println!("{}", item.greet());
}
1.3 Trait作為返回類型
fn create_greeter(name: String) -> impl Greet {Person { name }
}
二、關聯類型(Associated Types)
關聯類型是trait定義中的占位符類型,允許在實現trait時指定具體類型。
關聯類型(associated types)是一個將類型占位符與 trait 相關聯的方式,這樣 trait 的方法簽名中就可以使用這些占位符類型。
trait 的實現者會針對特定的實現在這個類型的位置指定相應的具體類型。如此可以定義一個使用多種類型的 trait,直到實現此 trait 時都無需知道這些類型具體是什么。
一個帶有關聯類型的 trait 的例子是標準庫提供的 Iterator trait。它有一個叫做 Item 的關聯類型來替代遍歷的值的類型。
pub trait Iterator {type Item;fn next(&mut self) -> Option<Self::Item>;
}
Item 是一個占位類型,同時 next 方法定義表明它返回 OptionSelf::Item 類型的值。
這個 trait 的實現者會指定 Item 的具體類型,然而不管實現者指定何種類型, next 方法都會返回一個包含了此具體類型值的 Option。
關聯類型看起來像一個類似泛型的概念,因為它允許定義一個函數而不指定其可以處理的類型。那么為什么要使用關聯類型呢?
讓我們通過一個Counter 結構體上實現 Iterator trait 的例子來檢視其中的區別。在如下示例中,指定了 Item 的類型為 u32:
trait Iterator {type Item; // 關聯類型,定義的時候無需知道具體是什么類型fn next(&mut self) -> Option<Self::Item>;
}struct Counter {count: u32,
}impl Iterator for Counter {type Item = u32; // 指定關聯類型的具體類型,實現的時候才需要指定具體類型fn next(&mut self) -> Option<Self::Item> {if self.count < 5 {self.count += 1;Some(self.count)} else {None}}
}fn main() {let mut counter = Counter { count: 0 };while let Some(value) = counter.next() {println!("count: {}", value);}
}
為啥不使用泛型?
這類似于泛型。那么為什么 Iterator trait 不像如下示例那樣定義呢?
pub trait Iterator<T> {fn next(&mut self) -> Option<T>;
}
區別在于當使用泛型時,則不得不在每一個實現中標注類型。
這是因為我們也可以實現為 Iterator<String> for Counter
,或任何其他類型,這樣就可以有多個 Counter 的 Iterator 的實現。
換句話說,當 trait 有泛型參數時,可以多次實現這個 trait,每次需改變泛型參數的具體類型。
接著當使用 Counter 的 next 方法時,必須提供類型標注來表明希望使用 Iterator 的哪一個實現。
當然,非要通過泛型也可以實現,結構體也設置成泛型
//泛型實現
struct Counter2<T> {count: T,
}trait Iterator2<T> {fn next(&mut self) -> Option<T>;
}impl Iterator2<i32> for Counter2<i32> {fn next(&mut self) -> Option<i32> {if self.count < 5 {self.count += 1;Some(self.count)} else {None}}
}impl Iterator2<f32> for Counter2<f32> {fn next(&mut self) -> Option<f32> {if self.count < 5.0 {self.count += 1.0;Some(self.count)} else {None}}
}impl Iterator2<String> for Counter2<String> {fn next(&mut self) -> Option<String> {if self.count.len() < 5 {self.count.push('a');Some(self.count.clone())} else {None}}
}fn main() {let mut counter2 = Counter2 { count: 0 };while let Some(value) = counter2.next() {println!("count: {}", value);}let mut counter3 = Counter2 { count: 0.0 };while let Some(value) = counter3.next() {println!("count: {}", value);}let mut counter4 = Counter2 { count: String::from("a") };while let Some(value) = counter4.next() {println!("count: {}", value);}
}
通過關聯類型,則無需標注類型,因為不能多次實現這個 trait。
對于使用關聯類型的定義,我們只能選擇一次 Item 會是什么類型,因為只能有一個 impl Iterator for Counter。
當調用 Counter 的 next 時不必每次指定我們需要 u32 值的迭代器。
三、默認泛型類型參數和運算符重載
當使用泛型類型參數時,可以為泛型指定一個默認的具體類型。
如果默認類型就足夠的話,這消除了為具體類型實現 trait 的需要。為泛型類型指定默認類型的語法是在聲明泛型類型時使用
<PlaceholderType=ConcreteType>
。
類似這樣
這種情況的一個非常好的例子是用于運算符重載。運算符重載(Operator overloading)是指在特定情況下自定義運算符(比如 +)行為的操作。
Rust 并不允許創建自定義運算符或重載任意運算符,不過 std::ops 中所列出的運算符 和相應的 trait 可以通過實現運算符相關 trait 來重載。
例如,如下示例中展示了如何在 Point 結構體上實現 Add trait 來重載 + 運算符,這樣就可以將兩個 Point 實例相加了:
use std::ops::Add;#[derive(Debug, PartialEq)]
struct Point {x: i32,y: i32,
}//這里不指定Add類型,默認是Self,也就是Point類型
impl Add for Point {type Output = Point; //指定關聯類型為我們定義的Point類型fn add(self, other: Point) -> Point {Point {x: self.x + other.x,y: self.y + other.y,}}
}fn main() {//運用加法println!("{:?}", Point { x: 1, y: 0 } + Point { x: 2, y: 3 });
}
add 方法將兩個 Point 實例的 x 值和 y 值分別相加來創建一個新的 Point。Add trait 有一個叫做 Output 的關聯類型,它用來決定 add 方法的返回值類型。
這里默認泛型類型位于 Add trait 中。這里是其定義:
這看來應該很熟悉,這是一個帶有一個方法和一個關聯類型的 trait。
比較陌生的部分是尖括號中的 RHS=Self:這個語法叫做 默認類型參數(default type parameters)。
RHS 是一個泛型類型參數(“right hand side” 的縮寫),它用于定義 add 方法中的 rhs 參數的類型。
如果實現 Add trait 時不指定 RHS 的具體類型,RHS 的類型將是默認的 Self 類型,也就是在其上實現 Add 的類型。
當為 Point 實現 Add 時,使用了默認的 RHS,因為我們希望將兩個 Point 實例相加。讓我們看看一個實現 Add trait 時希望自定義 RHS 類型而不是使用默認類型的例子。
這里有兩個存放不同單元值的結構體,Millimeters 和 Meters。我們希望能夠將毫米值與米值相加,并讓 Add 的實現正確處理轉換。
可以為 Millimeters 實現 Add 并以 Meters 作為 RHS,其實就是指定相加的兩個數右邊的類型為Meters
如下示例 所示。
use std::ops::Add;//定義毫米和米
#[derive(Debug)]
struct Millimeters(u32);
#[derive(Debug)]
struct Meters(u32);//為Millimeters實現Add trait
//指定other為Meters類型
impl Add<Meters> for Millimeters {type Output = Millimeters;fn add(self, other: Meters) -> Millimeters {Millimeters(self.0 + other.0 * 1000)}
}fn main() {let m = Millimeters(100);let n = Meters(1);//注意: 這兩個類型相加位置不能顛倒,因為我們為Millimeters實現了Add<Meters>,而不是Add<Millimeters>// let result = n + m; //報錯 cannot add `Millimeters` to `Meters`let result = m + n;println!("Result: {:?}", result);
}
毫米加米,得到毫米
為了使 Millimeters 和 Meters 能夠相加,我們指定 impl Add 來設定 RHS 類型參數的值而不是使用默認的 Self。
默認參數類型主要用于如下兩個方面:
- 擴展類型而不破壞現有代碼。
- 在大部分用戶都不需要的特定情況進行自定義。
標準庫的 Add trait 就是一個第二個目的例子:大部分時候你會將兩個相似的類型相加,不過它提供了自定義額外行為的能力。
在 Add trait 定義中使用默認類型參數意味著大部分時候無需指定額外的參數。
換句話說,一小部分實現的樣板代碼是不必要的,這樣使用 trait 就更容易了。
第一個目的是相似的,但過程是反過來的:如果需要為現有 trait 增加類型參數,為其提供一個默認類型將允許我們在不破壞現有實現代碼的基礎上擴展 trait 的功能。
四、完全限定語法與消歧義:調用相同名稱的方法
Rust 既不能避免一個 trait 與另一個 trait 擁有相同名稱的方法,也不能阻止為同一類型同時實現這兩個 trait。甚至直接在類型上實現開始已經有的同名方法也是可能的!
不過,當調用這些同名方法時,需要告訴 Rust 我們希望使用哪一個。
考慮如下示例中的代碼,這里定義了 trait Pilot 和 Wizard 都擁有方法 fly。
接著在一個本身已經實現了名為 fly 方法的類型 Human 上實現這兩個 trait。每一個 fly 方法都進行了不同的操作:
對于方法的完全限定調用
trait Pilot {fn fly(&self);
}trait Wizard {fn fly(&self);
}struct Human;impl Pilot for Human {fn fly(&self) {println!("This is your captain speaking.");}
}impl Wizard for Human {fn fly(&self) {println!("Up!");}
}impl Human {fn fly(&self) {println!("*waving arms furiously*");}
}//當調用 Human 實例的 fly 時,編譯器默認調用直接實現在類型上的方法
fn main() {let person = Human;//human實例直接調用fly方法,會調用impl Human中的fly方法,而不是Pilot和Wizard中的fly方法person.fly();
}
這表明 Rust 調用了直接實現在 Human 上的 fly 方法。
完全限定語法調用具體的方法
為了能夠調用 Pilot trait 或 Wizard trait 的 fly 方法,我們需要使用更明顯的語法以便能指定我們指的是哪個 fly 方法。這個語法展示在如下示例中:
fn main() {let person = Human;//human實例直接調用fly方法,會調用impl Human中的fly方法,而不是Pilot和Wizard中的fly方法person.fly();//使用完全限定語法來調用Pilot和Wizard中的fly方法Pilot::fly(&person);Wizard::fly(&person);
}
在方法名前指定 trait 名向 Rust 澄清了我們希望調用哪個 fly 實現。也可以選擇寫成 Human::fly(&person),這等同于person.fly(),不過如果無需消歧義的話這么寫就有點長了。
對于關聯函數的完全限定調用(沒有self參數):
因為 fly 方法獲取一個 self 參數,如果有兩個 類型 都實現了同一 trait,Rust 可以根據 self 的類型計算出應該使用哪一個 trait 實現。
然而,關聯函數是 trait 的一部分,但沒有 self 參數。
當同一作用域的兩個類型實現了同一 trait,Rust 就不能計算出我們期望的是哪一個類型,除非使用 完全限定語法(fully qualified syntax)。
例如,如下示例中的 Animal trait 來說,它有關聯函數 baby_name,結構體 Dog 實現了 Animal,同時有關聯函數 baby_name 直接定義于 Dog 之上:
trait Animal {fn baby_name() -> String;
}struct Dog;impl Dog {fn baby_name() -> String {String::from("Spot")}
}impl Animal for Dog {fn baby_name() -> String {String::from("puppy")}
}fn main() {println!("A baby dog is called {}", Dog::baby_name());
}
這段代碼用于一個動物收容所,他們將所有的小狗起名為 Spot,這實現為定義于 Dog 之上的關聯函數 baby_name。
Dog 類型還實現了 Animal trait,它描述了所有動物的共有的特征。小狗被稱為 puppy,這表現為 Dog 的 Animal trait 實現中與 Animal trait 相關聯的函數 baby_name。
在 main 調用了 Dog::baby_name 函數,它直接調用了定義于 Dog 之上的關聯函數。這段代碼會打印出:
這并不是我們需要的。我們希望調用的是 Dog 上 Animal trait 實現那部分的 baby_name 函數,這樣能夠打印出 A baby dog is called puppy。
如果將 main 改為在方法前面加上trait的方式,則會得到一個編譯錯誤:
因為 Animal::baby_name 是關聯函數而不是方法,因此它沒有 self 參數,Rust 無法計算出所需的是哪一個 Animal::baby_name 實現。我們會得到這個編譯錯誤:
為了消歧義并告訴 Rust 我們希望使用的是 Dog 的 Animal 實現,需要使用 完全限定語法,這是調用函數時最為明確的方式。如下示例展示了如何使用完全限定語法:
fn main() {//使用完全限定語法來調用Dog實現Animal trait的baby_name方法println!("A baby dog is called {}", <Dog as Animal>::baby_name());
}
我們在尖括號中向 Rust 提供了類型標注,并通過在此函數調用中將 Dog 類型當作 Animal 對待,來指定希望調用的是 Dog 上 Animal trait 實現中的 baby_name 函數。
現在這段代碼會打印出我們期望的數據:
通常,完全限定語法定義為:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
對于關聯函數,其沒有一個 receiver,故只會有其他參數的列表。可以選擇在任何函數或方法調用處使用完全限定語法。
然而,允許省略任何 Rust 能夠從程序中的其他信息中計算出的部分。只有當存在多個同名實現而 Rust 需要幫助以便知道我們希望調用哪個實現時,才需要使用這個較為冗長的語法。
五、父 trait 用于在另一個 trait 中使用某 trait 的功能
有時我們可能會需要某個 trait 使用另一個 trait 的功能。
在這種情況下,需要能夠依賴相關的 trait 也被實現。這個所需的 trait 是我們實現的 trait 的 父(超) trait(supertrait)。
語法:
trait1: trait2
trait1就是子trait, trait2是父trait
要求實現子 trait 的類型必須實現所有 supertrait。在 Rust 中,被繼承的 trait 稱為 supertrait。實現子 trait 時,必須已經實現了所有 supertrait
例如我們希望創建一個帶有 outline_print 方法的 trait OutlinePrint,它會打印出帶有星號框的值。
也就是說,如果 Point 實現了 Display 并返回 (x, y),調用以 1 作為 x 和 3 作為 y 的 Point 實例的 outline_print 會顯示如下:
實現Display,只需要實現fmt方法
//子trait父trait
use std::fmt::Display;//自定義OutlinePrint trait 繼承Display trait,要求實現Display trait
trait OutlinePrint: Display {fn outline_print(&self) {let output = self.to_string();let len = output.len();println!("{}", "*".repeat(len + 4));println!("*{}*", " ".repeat(len + 2));println!("* {} *", output);println!("*{}*", " ".repeat(len + 2));println!("{}", "*".repeat(len + 4));}
}struct Point {x: i32,y: i32,
}//為Point實現Display trait,只需要實現fmt方法
impl Display for Point {fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {write!(f, "({}, {})", self.x, self.y)}
}impl OutlinePrint for Point {}fn main() {let p = Point { x: 1, y: 3 };p.outline_print();
}
六、newtype 模式用于在外部類型上實現外部 trait
之前我們提到了孤兒規則(orphan rule),它說明只要 trait 或類型對于當前 crate 在本地的話就可以在此類型上實現該 trait。
一個繞開這個限制的方法是使用 newtype 模式(newtype pattern),它涉及到在一個元組結構體中創建一個新類型。
這個元組結構體帶有一個字段作為希望實現 trait 的類型的簡單封裝。
接著這個封裝類型對于 crate 是本地的,這樣就可以在這個封裝上實現 trait。
Newtype 是一個源自 Haskell 編程語言的概念。使用這個模式沒有運行時性能消耗,這個封裝類型在編譯時就被省略了。
例如,如果想要在 Vec<T>
上實現 Display,而孤兒規則阻止我們直接這么做,因為 Display trait 和 Vec<T>
都定義于我們的 crate 之外。
可以創建一個包含 Vec<T>
實例的 Wrapper 結構體,接著可以如下示例 那樣在 Wrapper 上實現 Display 并使用 Vec<T>
的值:
//外部類型實現外部trait
use std::fmt;//創建一個包含外部類型Vec的元組結構體Wrapper
struct Wrapper(Vec<String>);//為Wrapper實現Display trait
impl fmt::Display for Wrapper {fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {//以逗號相連成[]包裹的字符串write!(f, "[{}]", self.0.join(", "))}
}fn main() {let w = Wrapper(vec![String::from("hello"), String::from("world")]);println!("w = {}", w);
}
Display 的實現使用 self.0 來訪問其內部的 Vec,因為 Wrapper 是元組結構體而 Vec 是結構體總位于索引 0 的項。接著就可以使用 Wrapper 中 Display 的功能了。
此方法的缺點是,因為 Wrapper 是一個新類型,它沒有定義于其值之上的方法;
必須直接在 Wrapper 上實現 Vec 的所有方法,這樣就可以代理到self.0 上 —— 這就允許我們完全像 Vec 那樣對待 Wrapper。
如果希望新類型擁有其內部類型的每一個方法,為封裝類型實現 Deref trait 并返回其內部類型是一種解決方案。
如果不希望封裝類型擁有所有內部類型的方法 —— 比如為了限制封裝類型的行為 —— 則必須只自行實現所需的方法。