喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=^・ω・^=)
19.2.1. 在trait定义中使用关联类型来指定占位类型
我们首先在第10章的10.3. trait Pt.1:trait的定义、约束与实现 和 10.4. trait Pt.2:trait作为参数和返回类型、trait bound 中介绍了trait,但我们没有讨论更高级的细节。现在我们来深入了解
关联类型(associated type)是trait中的类型占位符,它可以用于trait方法的签名中。它用于定义出包某些类型的trait,而在实现前无需知道这些类型是什么。
看个例子:
rust
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
标准库中的负责迭代器部分的iterator
trait(详见 13.8. 迭代器 Pt.4:创建自定义迭代器)就是一个带有关联类型的trait
,其定义如上。
Item
就是关联类型。在迭代过程中使用Item
类型来替代实际出现的值以完成逻辑和实际数据类型分离的目的。可以看到next
方法的返回值Option<Self:Item>
就出现了Item
。
Item
就是所谓的类型占位符,其核心思想与泛型有点像,但区别也是有的:
泛型 | 关联类型 |
---|---|
每次实现 Trait 时标注类型 | 无需标注类型 |
可以为一个类型多次实现某个 Trait(不同的泛型参数) | 无法为单个类型多次实现某个 Trait |
19.2.2. 默认泛型参数和运算符重载
我们可以在使用泛型参数时为泛型指定一个默认的具体类型。它的语法是<PlaceholderType=ConcreteType>
。这种技术常用于运算符重载(operator overloading)。
虽然Rust不允许创建自己的运算符及重载任意的运算符,但是可以通过实现std::ops
中列出的那些trait来重载一部分相应的运算符。
看个例子:
rust
use std::ops::Add;
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
assert_eq!(
Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
Point { x: 3, y: 3 }
);
}
- 我们在这个例子中为
point
结构体实现了Add
trait,也就是重载了+
这个运算符,具体来说就是Add
trait下的add
函数把每个字段分别相加 - 主函数里就可以直接使用
+
来把两个Point
类型相加
Add
trait的定义如下:
rust
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
它就使用了默认的泛型参数类型Rhs=Self
。也就是说当我们实现Add
trait时如果没有为Rhs
指定一个具体的类型,那么Rhs
的类型就默认为Self
,所以上文的例子中的Rhs
就是Point
。
再看一个例子,这回我们想要实现毫米和米相加的例子:
rust
use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
- 这里先通过结构体字段声明了
Millimeters
和Meters
,分别表示毫米和米。 - 下文为
Millimeters
实现了Add
trait,其中又通过<Meters>
显式地指明了类型被设定为Meters
了。add
函数中通过本身的毫米和传进来的米乘1000相加得出来以毫米计数的值。
19.2.3. 默认泛型参数的主要应用场景
- 扩展一个类型而不破坏现有代码
- 允许在大部分用户都不需要的特定场景下进行自定义
19.2.4. 完全限定语法(Fully Qualified Syntax)如何调用同名方法
直接看例子:
rust
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*");
}
}
- 定义了两个trait,分别叫
Pilot
和Wizard
,都有一个fly
方法,没有具体实现 - 有一个结构体叫
Human
,我们在下文为它分别实现了那两个trait,也就是分别为两个trait写了fly
方法。除此之外,还通过impl
块为结构体本身实现了fly
方法。
这个时候一共有三个fly
方法,如果我们在主函数中调用:
rust
fn main() {
let person = Human;
person.fly();
}
运行这段代码会打印出*waving arms furiously*
,表明Rust直接调用了Human
上实现的fly
方法。
要从Pilot
trait或Wizard
trait调用fly
方法,我们需要使用更明确的语法来指定我们指的是哪个fly
方法:
rust
fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();
}
在方法名称之前指定特征名称可以向 Rust 阐明我们要调用哪个fly
实现。person.fly()
也可以写 Human::fly(&person)
。
输出:
This is your captain speaking.
Up!
*waving arms furiously*
但是,不是方法的关联函数没有self
范围。当有多个类型或trait定义的方法具有相同函数名时,Rust 并不总是知道指的是哪种类型,除非使用完全限定语法:
rust
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 a {}", Dog::baby_name());
}
Animal
trait有baby_name
方法。Dog
是一个结构体,实现了Animal
trait,同时也通过impl
块实现了baby_name
方法。这是一共就有两个baby_name
方法。- 在主函数里使用了
Dog::baby_name()
,按照上文的逻辑就应该执行Dog
的impl
块实现的baby_name
方法,也就是输出"Spot"。
输出:
A baby dog is called a Spot
那怎么实现Dog
实现的Animal
trait上的baby_name
方法呢?我们试试使用上一个例子的逻辑:
rust
fn main() {
println!("A baby dog is called a {}", Animal::baby_name());
}
输出:
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
--> src/main.rs:20:43
|
2 | fn baby_name() -> String;
| ------------------------- `Animal::baby_name` defined here
...
20 | println!("A baby dog is called a {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
|
help: use the fully-qualified path to the only available implementation
|
20 | println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
| +++++++ +
For more information about this error, try `rustc --explain E0790`.
error: could not compile `traits-example` (bin "traits-example") due to 1 previous error
Animal
trait上的baby_name
函数的执行需要知道是哪个类型上的实现,但是baby_name
这个方法又没有参数,所以不知道是哪个类型上的实现。
针对这种情况就得使用完全限定语法。其形式为:
rust
<Type as Trait>::function(receiver_if_method, next_arg, ...);
这种语法可以在任何调用函数或方法的地方使用,并且它允许忽略那些从其它上下文推导出来的部分。
但是这种语法只有在Rust无法区分你期望调用哪个具体实现的时候才需要使用这种语法,因为这种语法写起来太麻烦了,所以轻易不使用。
根据这个语法上面的代码就应该这么改:
rust
fn main() {
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
输出:
A baby dog is called a puppy
19.2.5. 使用supertrait来要求trait附带其它trait的功能
有时候我们可能会需要在一个trait中使用其它trait的功能,也就是说间接依赖的trait也需要被实现。而那个被间接依赖的trait
就是当前trait的supertrait。
看个例子:
rust
use std::fmt;
trait OutlinePrint: fmt::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));
}
}
OutlinePrint
实际上是用来在终端通过字符打印一个图形的。但是在打印过程中必须要求self
实现了to_string
方法,也就是要求self
实现了Display
trait(to_string
是Display
trait下的方法)。其写法就是trait
关键字 + trait名字 + :
+ supertrait。
假如我们有一个结构体Point
,想要通过OutlinePrint
trait的outline_print
函数在终端打印出来。又因为OutlinePrint
trait需要Display
trait的函数,所以得为它同时实现OutlinePrint
trait和Display
trait,不然就会报错:
rust
struct Point {
x: i32,
y: i32,
}
use std::fmt;
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
impl OutlinePrint for Point {}
19.2.6. 使用newtype模式在外部类型上实现外部trait
我们之前讲过一个孤儿规则:只有当trait
或类型定义在本地包时,才能为该类型实现这个trait。而我们可以使用newtype
模式来绕过这一规则,具体来说就是利用元组结构体来构建一个新的类型放在本地。
看个例子:
我们想为Vector
实现Display
trait,但是Vector
和Display
trait都定义在在外部包中,所以无法直接为Vector
实现。所以把Vector
包裹在自己创建的元组结构体Wrapper
里,然后用Wrapper
来实现Display
trait:
rust
use std::fmt;
struct Wrapper(Vec<String>);
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}");
}