✨✨ 欢迎大家来到景天科技苑✨✨
🎈🎈 养成好习惯,先赞后看哦~🎈🎈
🏆 作者简介:景天科技苑
🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,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 并返回其内部类型是一种解决方案。
如果不希望封装类型拥有所有内部类型的方法 —— 比如为了限制封装类型的行为 —— 则必须只自行实现所需的方法。