高级特征
我们首先在“特征:定义共享
Behavior“部分,但我们没有讨论更高级的细节。现在你对 Rust 有了更多的了解,我们可以深入了解细节。
使用关联类型在 Trait Definitions 中指定占位符类型
关联类型将类型占位符与 trait 连接起来,以便 trait 方法定义可以在其签名中使用这些占位符类型。trait 的实现者将指定要使用的具体类型,而不是特定实现的占位符类型。这样,我们就可以定义一个使用某些类型的 trait,而不需要确切地知道这些类型是什么,直到 trait 被实现。
我们在本章中介绍了很少需要的大多数高级功能。关联类型介于两者之间:它们比本书其余部分解释的功能使用得更频繁,但比本章讨论的许多其他功能更常见。
具有关联类型的 trait 的一个示例是标准库提供的 Iterator
trait。关联的类型名为 Item
,它代替了实现 Iterator
trait 的类型正在迭代的值的类型。Iterator
trait 的定义如示例 19-12 所示。
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
示例 19-12:具有关联类型 Item
的 Iterator
trait 的定义
Type Item
是一个占位符,下一个
方法的定义显示它将返回 Option<Self::Item>
类型的值。的
Iterator
trait 将指定 Item
的具体类型,下一个
method 将返回一个包含该具体类型值的 Option
。
关联类型似乎与泛型的概念相似,因为泛型允许我们定义一个函数,而无需指定它可以处理哪些类型。为了检查这两个概念之间的区别,我们将查看 Iterator
trait 在名为 Counter
的类型上的实现,该类型指定 Item
类型为 u32
:
文件名: src/lib.rs
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
// --snip--
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
此语法似乎与泛型的语法相当。那么为什么不直接定义
Iterator
trait 替换为泛型,如示例 19-13 所示?
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
示例 19-13:
使用泛型的 Iterator
trait
区别在于,当使用泛型时,如示例 19-13 所示,我们必须
注释每个 implementation中的类型;因为我们也可以实现
Iterator<String> 对于 Counter
或任何其他类型,我们可以有多个 Iterator
for Counter
的实现。换句话说,当特征具有
generic 参数,它可以多次为一个类型实现,更改
每次泛型类型参数的具体类型。当我们使用
next
方法,我们必须
提供类型注释来指示我们想要使用的 Iterator
的哪个实现。
使用关联类型,我们不需要注释类型,因为我们不能
多次在一个类型上实现一个 trait。在示例 19-12 中,使用
定义,我们只能选择
Item
将是一次,因为 Counter 只能有一个 impl Iterator
。我们不必指定我们想要一个 u32
值的迭代器,我们在 Counter
上调用 next
。
关联类型也成为 trait 契约的一部分:trait 的实现者必须提供一个类型来代替关联类型占位符。关联类型通常具有描述如何使用该类型的名称,在 API 文档中记录关联类型是一种很好的做法。
默认泛型类型参数和运算符重载
当我们使用泛型类型参数时,我们可以为泛型类型指定默认的具体类型。如果默认类型有效,则 trait 的实现者无需指定具体类型。使用 <PlaceholderType=ConcreteType>
语法声明泛型类型时,您可以指定默认类型。
此技术有用的一个很好的示例是使用运算符
重载,其中自定义运算符(如 +
)在特定情况下的行为。
Rust 不允许你创建自己的运算符或重载任意运算符。但是,你可以通过实现与运算符关联的 trait 来重载 std::ops
中列出的作和相应的 trait。例如,在示例 19-14 中,我们重载了 +
运算符以添加两个 Point
实例。我们通过在 Point
上实现 Add
trait 来实现这一点
结构:
文件名: src/main.rs
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 } ); }
示例 19-14:实现 Add
trait 以重载 Point
实例的 +
运算符
add
方法将两个 Point
实例的 x
值和 y
相加
值来创建新的
Point
。Add
trait 具有名为 Output
的关联类型,用于确定从 add
返回的类型
方法。
此代码中的默认泛型类型位于 Add
trait 中。以下是它的定义:
#![allow(unused)] fn main() { trait Add<Rhs=Self> { type Output; fn add(self, rhs: Rhs) -> Self::Output; } }
这段代码应该看起来通常很熟悉:一个具有一个方法和一个关联类型的 trait。新部分是 Rhs=Self
:此语法称为 default
类型参数。Rhs
泛型类型参数(“right hand side”的缩写)定义 add
方法中 rhs
参数的类型。如果我们在实现 Add
trait 时没有为 Rhs
指定具体类型,则 Rhs
的类型将默认为 Self
,这将是我们正在实现的类型
附加
。
当我们实现 Add
for Point
时,我们使用了 Rhs
的默认值,因为我们想添加两个 Point
实例。让我们看一个实现 Add
trait 的示例,其中我们想要自定义 Rhs
类型而不是使用默认类型。
我们有两个结构体,Millimeters
和 Meters
,它们保存不同
单位。将现有类型在另一个结构体中的这种薄包装称为
newtype 模式,我们将在“使用 Newtype
Pattern to Implement External traits on External Types“部分。我们希望将以毫米为单位的值与以米为单位的值相加,并让 Add
的实现正确地进行转换。我们可以实现 Add
以 Meters
作为 Rhs
的 Millimeters
,如示例 19-15 所示。
文件名: src/lib.rs
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))
}
}
示例 19-15:在
Millimeters (毫米
) 将 Millimeters
(毫米) 添加到 Meters (米)
要添加毫米
和米
,我们指定 impl Add<Meters>
来设置 Rhs
类型参数的值,而不是使用默认值 Self
。
您将以两种主要方式使用默认类型参数:
在不破坏现有代码的情况下扩展类型
为了允许在特定情况下进行自定义,大多数用户不需要
标准库的 Add
trait 是第二个目的的一个示例:通常,您将添加两个 like 类型,但 Add
trait 提供了在此范围之外进行自定义的功能。在 Add
trait 定义中使用默认类型参数意味着大多数情况下您不必指定额外的参数。换句话说,不需要一些 implementation 样板,从而更容易使用 trait。
第一个目的与第二个目的类似,但相反:如果要向现有 trait 添加 type 参数,则可以为其提供默认值,以允许在不破坏现有实现代码的情况下扩展 trait 的功能。
消除歧义的完全限定语法:调用具有相同名称的方法
Rust 中没有任何内容可以阻止一个 trait 具有与另一个 trait 的方法同名的方法,Rust 也不会阻止你在一种类型上实现这两个 trait。也可以直接在类型上实现一个方法,其名称与 trait 中的方法相同。
当调用具有相同名称的方法时,你需要告诉 Rust 你是哪一个
想要使用。考虑示例 19-16 中的代码,我们定义了两个 trait,
Pilot
和 Wizard
,它们都有一个叫做 fly
的方法。然后,我们在 Human
类型上实现这两个 trait,该类型已经实现了一个名为 fly
的方法。每种 fly
方法的作用都不同。
文件名: src/main.rs
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*"); } } fn main() {}
示例 19-16:定义了两个 trait 来具有 fly
方法并在 Human
类型上实现,而 fly
方法直接在 Human
上实现
当我们在 Human
实例上调用 fly
时,编译器默认调用直接在该类型上实现的方法,如示例 19-17 所示。
文件名: src/main.rs
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*"); } } fn main() { let person = Human; person.fly(); }
示例 19-17:在
人
运行这段代码会打印出 *挥舞手臂 farriously*
,表明 Rust 直接调用了在 Human
上实现的 fly
方法。
要从 Pilot
trait 或 Wizard
trait 调用 fly
方法,我们需要使用更明确的语法来指定我们指的是哪种 fly
方法。示例 19-18 演示了此语法。
文件名: src/main.rs
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*"); } } fn main() { let person = Human; Pilot::fly(&person); Wizard::fly(&person); person.fly(); }
示例 19-18:指定要调用的 trait 的 fly
方法
在方法名称之前指定 trait name 可以向 Rust 阐明我们要调用哪个 fly
实现。我们也可以编写
Human::fly(&person) 的 Human::fly(&person)
等同于示例 19-18 中使用的 person.fly(),
但是如果我们不需要消除歧义,写起来会有点长。
运行此代码将打印以下内容:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.46s
Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*
因为 fly
方法接受 self
参数,如果我们有两个类型都实现一个 trait,Rust 可以根据 self
的类型来确定要使用哪个 trait 实现。
但是,不是方法的关联函数没有 self
参数。当有多个类型或特征定义非方法
函数具有相同的函数名称,Rust 并不总是知道你是哪种类型
表示,除非你使用完全限定的语法。例如,在示例 19-19 中,我们为一个动物收容所创建了一个 trait,该动物收容所希望将所有幼犬命名为 Spot。我们使用关联的非方法函数 baby_name
创建一个 Animal
trait。Animal
trait 是为结构体 Dog
实现的,我们还直接在其上提供了一个关联的非方法函数baby_name
。
文件名: src/main.rs
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()); }
示例 19-19:具有关联函数的 trait 和具有同名关联函数的类型,该函数也实现了该 trait
我们在 Dog
上定义的 baby_name
关联函数中实现将所有小狗命名为 Spot 的代码。Dog
类型也实现了 trait
Animal
,描述所有动物都具有的特征。小狗被称为小狗,这在 Animal
的实现中得到了体现
trait 在 Animal
trait 关联的
baby_name
函数中。
在 main
中,我们调用 Dog::baby_name
函数,该函数直接调用 Dog
上定义的关联函数。此代码打印以下内容:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s
Running `target/debug/traits-example`
A baby dog is called a Spot
这个输出不是我们想要的。我们想调用 baby_name
函数,它是我们在 Dog
上实现的 Animal
trait 的一部分,因此代码打印
婴儿狗被称为小狗
。我们在示例 19-18 中使用的指定 trait name 的技术在这里没有帮助;如果我们将 main
改为示例 19-20 中的代码,我们将得到一个编译错误。
文件名: src/main.rs
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 {}", Animal::baby_name());
}
示例 19-20:尝试调用 baby_name
function 的 Animal
trait 中,但 Rust 不知道该使用哪个实现
因为 Animal::baby_name
没有 self
参数,并且可能还有其他类型实现了 Animal
trait,所以 Rust 无法弄清楚我们想要哪个 Animal::baby_name
实现。我们将收到这个编译器错误:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
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
为了消除歧义并告诉 Rust 我们想使用
Animal
for Dog
与其他类型的 Animal
实现相反,我们需要使用完全限定的语法。示例 19-21 演示了如何使用完全限定语法。
文件名: src/main.rs
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 as Animal>::baby_name()); }
示例 19-21:使用完全限定语法指定我们想从 Dog
上实现的 Animal
trait 调用 baby_name
函数
我们在尖括号内为 Rust 提供了一个类型注释,这表明我们想从 Dog
上实现的 Animal
trait 调用 baby_name
方法,方法是说我们想将 Dog
类型视为
Animal
进行此函数调用。这段代码现在将打印我们想要的内容:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/traits-example`
A baby dog is called a puppy
通常,完全限定语法定义如下:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
对于不是方法的关联函数,不会有接收器
:只有其他参数的列表。您可以在调用函数或方法的任何位置使用完全限定的语法。但是,你可以省略 Rust 可以从程序中的其他信息中找出的语法的任何部分。你只需要在有多个使用相同名称的实现的情况下使用这种更详细的语法,并且 Rust 需要帮助来确定你想要调用的实现。
使用 supertrait 要求一个特征在另一个特征中的功能
有时,你可能会编写一个依赖于另一个 trait 的 trait 定义:对于要实现第一个 trait 的类型,你希望要求该类型也实现第二个 trait。这样做是为了让您的特征定义可以使用第二个特征的关联项目。您的特征定义所依赖的特征称为 supertrait of your trait.
例如,假设我们想让 OutlinePrint
特征具有
outline_print
一种方法,它将打印一个给定的值,其格式使其以星号为框架。也就是说,给定一个 Point
结构体,该结构体实现了标准库 trait Display
以产生 (x, y),
当我们调用
outline_print
在 1 表示 x
且 3
表示
y
的 Point
实例上,它应打印以下内容:
**********
* *
* (1, 3) *
* *
**********
在 outline_print
方法的实现中,我们希望使用
显示
特征的功能。因此,我们需要指定
OutlinePrint
trait 仅适用于同时实现 Display
并提供 OutlinePrint
所需功能的类型。我们可以在 trait 定义中通过指定 OutlinePrint: Display
来做到这一点。此技术类似于添加绑定到 trait 的 trait。示例 19-22 显示了 OutlinePrint
trait 的实现。
文件名: src/main.rs
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)); } } fn main() {}
示例 19-22:实现需要 Display
功能的 OutlinePrint
trait
因为我们已经指定 OutlinePrint
需要 Display
trait,所以我们可以使用 to_string
函数,该函数会自动实现任何实现 Display
的类型。如果我们尝试使用 to_string
而不添加冒号并在 trait 名称后指定 Display
trait,我们将收到一个错误,指出在当前范围内没有找到类型 &
Self 的名为 to_string
的方法。
让我们看看当我们尝试在未实现 Display
的类型(例如 Point
结构体)上实现 OutlinePrint
时会发生什么情况:
文件名: src/main.rs
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));
}
}
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
fn main() {
let p = Point { x: 1, y: 3 };
p.outline_print();
}
我们收到一条错误消息,指出 Display
是必需的,但未实现:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:20:23
|
20 | impl OutlinePrint for Point {}
| ^^^^^ `Point` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
note: required by a bound in `OutlinePrint`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint`
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:24:7
|
24 | p.outline_print();
| ^^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
note: required by a bound in `OutlinePrint::outline_print`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint::outline_print`
4 | fn outline_print(&self) {
| ------------- required by a bound in this associated function
For more information about this error, try `rustc --explain E0277`.
error: could not compile `traits-example` (bin "traits-example") due to 2 previous errors
为了解决这个问题,我们实现了 Display
on Point
并满足
OutlinePrint
需要,如下所示:
文件名: src/main.rs
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)); } } struct Point { x: i32, y: i32, } impl OutlinePrint for Point {} use std::fmt; impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) } } fn main() { let p = Point { x: 1, y: 3 }; p.outline_print(); }
然后,在 Point
上实现 OutlinePrint
特征将成功编译,我们可以在 Point
实例上调用 outline_print
以在星号轮廓中显示它。
使用 newtype 模式在外部类型上实现 external trait
在第 10 章的 “在
Type“ 部分,我们提到了孤立规则,该规则规定只有当 trait 或 type 是 crate 的本地类型时,我们才允许在类型上实现 trait。可以使用 newtype 模式来绕过这个限制,这涉及在 Tuples 结构体中创建新类型。(我们在“使用 Tuple
没有命名字段的结构体来创建不同类型的“部分。元组结构将有一个字段,并且是一个
thin 包装器。然后,包装器
type 是我们的 crate 的本地类型,我们可以在 wrapper 上实现 trait。
Newtype 是一个源自 Haskell 编程语言的术语。使用此模式不会对运行时性能造成影响,并且在编译时省略了包装器类型。
例如,假设我们想在 Vec<T>
上实现 Display
,孤立规则阻止我们直接执行此作,因为 Display
特征和
Vec<T>
类型在我们的 crate 之外定义。我们可以创建一个 Wrapper
结构体,其中包含 Vec<T>
的实例;那么我们就可以在
Wrapper
并使用 Vec<T>
值,如示例 19-23 所示。
文件名: src/main.rs
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}"); }
示例 19-23:围绕
Vec<String>
实现显示
Display
的实现使用 self.0
来访问内部 Vec<T>
,因为 Wrapper
是一个元组结构,而 Vec<T>
是元组中索引为 0 的项目。然后,我们可以在 Wrapper
上使用 Display
trait 的功能。
使用这种技术的缺点是 Wrapper
是一种新类型,因此它没有它所持有的值的方法。我们必须直接在 Wrapper
上实现 Vec<T>
的所有方法,以便这些方法委托给 self.0
,这将允许我们将 Wrapper
完全视为
Vec<T>
.如果我们希望新类型具有内部类型所具有的所有方法,则实现 Deref
trait(在第 15 章的“处理智能
指针(如具有 Deref
的常规引用)
Trait“ 部分)返回内部类型将是一个解决方案。如果我们不希望
Wrapper
类型具有内部类型的所有方法(例如,为了限制 Wrapper
类型的行为),我们将不得不手动实现我们确实需要的方法。
即使不涉及 trait,这种 newtype 模式也很有用。让我们转移焦点,看看一些与 Rust 的类型系统交互的高级方法。