高级 Trait
Advanced Traits
我们首先在第 10 章的“使用 trait 定义共享行为”部分介绍了 trait,但没有讨论更高级的细节。既然你对 Rust 有了更多了解,我们可以深入研究其中的细节。
We first covered traits in the “Defining Shared Behavior with Traits” section in Chapter 10, but we didn’t discuss the more advanced details. Now that you know more about Rust, we can get into the nitty-gritty.
使用关联类型在 trait 定义中指定占位符类型
Defining Traits with Associated Types
关联类型 (Associated types) 将类型占位符与 trait 连接起来,使得 trait 方法定义可以在其签名中使用这些占位符类型。trait 的实现者将为特定的实现指定要用于替代占位符类型的具体类型。这样,我们就可以定义一个使用某些类型的 trait,而无需在实现 trait 之前确切地知道这些类型是什么。
Associated types connect a type placeholder with a trait such that the trait method definitions can use these placeholder types in their signatures. The implementor of a trait will specify the concrete type to be used instead of the placeholder type for the particular implementation. That way, we can define a trait that uses some types without needing to know exactly what those types are until the trait is implemented.
我们曾将本章中的大多数高级功能描述为很少需要的功能。关联类型介于两者之间:它们的使用频率低于本书其余部分解释的功能,但比本章讨论的许多其他功能更常见。
We’ve described most of the advanced features in this chapter as being rarely needed. Associated types are somewhere in the middle: They’re used more rarely than features explained in the rest of the book but more commonly than many of the other features discussed in this chapter.
具有关联类型的 trait 的一个例子是标准库提供的 Iterator trait。关联类型被命名为 Item,代表实现 Iterator trait 的类型正在迭代的值的类型。Iterator trait 的定义如示例 20-13 所示。
One example of a trait with an associated type is the Iterator trait that the
standard library provides. The associated type is named Item and stands in
for the type of the values the type implementing the Iterator trait is
iterating over. The definition of the Iterator trait is as shown in Listing
20-13.
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
类型 Item 是一个占位符,next 方法的定义显示它将返回 Option<Self::Item> 类型的值。Iterator trait 的实现者将为 Item 指定具体类型,而 next 方法将返回包含该具体类型值的 Option。
The type Item is a placeholder, and the next method’s definition shows that
it will return values of type Option<Self::Item>. Implementors of the
Iterator trait will specify the concrete type for Item, and the next
method will return an Option containing a value of that concrete type.
关联类型看起来像是与泛型类似的概念,因为后者允许我们定义一个函数而不指定它可以处理哪些类型。为了检查这两个概念之间的区别,我们将看看在名为 Counter 的类型上实现的 Iterator trait,该类型指定 Item 类型为 u32:
Associated types might seem like a similar concept to generics, in that the
latter allow us to define a function without specifying what types it can
handle. To examine the difference between the two concepts, we’ll look at an
implementation of the Iterator trait on a type named Counter that specifies
the Item type is u32:
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 呢,如示例 20-14 所示?
This syntax seems comparable to that of generics. So, why not just define the
Iterator trait with generics, as shown in Listing 20-14?
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
区别在于,当使用泛型时(如示例 20-14 所示),我们必须在每个实现中标注类型;因为我们还可以为 Counter 实现 Iterator<String> 或任何其他类型,所以我们可以为 Counter 拥有 Iterator 的多个实现。换句话说,当一个 trait 具有泛型参数时,它可以为一个类型实现多次,每次更改泛型类型参数的具体类型。当我们在 Counter 上使用 next 方法时,我们将不得不提供类型标注,以指示我们要使用 Iterator 的哪种实现。
The difference is that when using generics, as in Listing 20-14, we must
annotate the types in each implementation; because we can also implement
Iterator<String> for Counter or any other type, we could have multiple
implementations of Iterator for Counter. In other words, when a trait has a
generic parameter, it can be implemented for a type multiple times, changing
the concrete types of the generic type parameters each time. When we use the
next method on Counter, we would have to provide type annotations to
indicate which implementation of Iterator we want to use.
使用关联类型,我们不需要标注类型,因为我们无法为一个类型多次实现同一个 trait。在示例 20-13 使用关联类型的定义中,我们只能选择一次 Item 的类型,因为只能有一个 impl Iterator for Counter。我们不需要在每次调用 Counter 的 next 方法时都指定我们想要一个 u32 值的迭代器。
With associated types, we don’t need to annotate types, because we can’t
implement a trait on a type multiple times. In Listing 20-13 with the
definition that uses associated types, we can choose what the type of Item
will be only once because there can be only one impl Iterator for Counter. We
don’t have to specify that we want an iterator of u32 values everywhere we
call next on Counter.
关联类型也成为 trait 契约的一部分:trait 的实现者必须提供一个类型来替代关联类型占位符。关联类型通常有一个名称来描述该类型的用途,并在 API 文档中记录关联类型是一个很好的做法。
Associated types also become part of the trait’s contract: Implementors of the trait must provide a type to stand in for the associated type placeholder. Associated types often have a name that describes how the type will be used, and documenting the associated type in the API documentation is a good practice.
默认泛型参数和运算符重载
Using Default Generic Parameters and Operator Overloading
当我们使用泛型类型参数时,可以为该泛型类型指定一个默认的具体类型。如果默认类型有效,这消除了 trait 实现者指定具体类型的需要。在声明泛型类型时,使用 <PlaceholderType=ConcreteType> 语法指定默认类型。
When we use generic type parameters, we can specify a default concrete type for
the generic type. This eliminates the need for implementors of the trait to
specify a concrete type if the default type works. You specify a default type
when declaring a generic type with the <PlaceholderType=ConcreteType> syntax.
这种技术非常有用的一个很好的例子是运算符重载 (operator overloading),即在特定情况下自定义运算符(如 +)的行为。
A great example of a situation where this technique is useful is with operator
overloading, in which you customize the behavior of an operator (such as +)
in particular situations.
Rust 不允许你创建自己的运算符或重载任意运算符。但你可以通过实现与运算符相关的 trait 来重载 std::ops 中列出的操作和对应的 trait。例如,在示例 20-15 中,我们重载了 + 运算符,将两个 Point 实例相加。我们通过在 Point 结构体上实现 Add trait 来做到这一点。
Rust doesn’t allow you to create your own operators or overload arbitrary
operators. But you can overload the operations and corresponding traits listed
in std::ops by implementing the traits associated with the operator. For
example, in Listing 20-15, we overload the + operator to add two Point
instances together. We do this by implementing the Add trait on a Point
struct.
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 }
);
}
add 方法将两个 Point 实例的 x 值和两个 Point 实例的 y 值相加,创建一个新的 Point。Add trait 有一个名为 Output 的关联类型,用于确定 add 方法返回的类型。
The add method adds the x values of two Point instances and the y
values of two Point instances to create a new Point. The Add trait has an
associated type named Output that determines the type returned from the add
method.
此代码中的默认泛型类型位于 Add trait 中。这是它的定义:
The default generic type in this code is within the Add trait. Here is its
definition:
#![allow(unused)]
fn main() {
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
}
这段代码看起来应该比较熟悉:一个具有一个方法和一个关联类型的 trait。新部分是 Rhs=Self:这种语法被称为默认类型参数 (default type parameters)。Rhs 泛型类型参数(“right-hand side”的缩写)定义了 add 方法中 rhs 参数的类型。如果在实现 Add trait 时不为 Rhs 指定具体类型,则 Rhs 的类型将默认为 Self,即我们正在为其实现 Add 的类型。
This code should look generally familiar: a trait with one method and an
associated type. The new part is Rhs=Self: This syntax is called default
type parameters. The Rhs generic type parameter (short for “right-hand
side”) defines the type of the rhs parameter in the add method. If we don’t
specify a concrete type for Rhs when we implement the Add trait, the type
of Rhs will default to Self, which will be the type we’re implementing
Add on.
当我们为 Point 实现 Add 时,我们使用了 Rhs 的默认值,因为我们想将两个 Point 实例相加。让我们看一个实现 Add trait 的示例,在这个示例中,我们想要自定义 Rhs 类型而不是使用默认值。
When we implemented Add for Point, we used the default for Rhs because we
wanted to add two Point instances. Let’s look at an example of implementing
the Add trait where we want to customize the Rhs type rather than using the
default.
我们有两个结构体 Millimeters 和 Meters,它们持有不同单位的值。这种将现有类型薄薄地包装在另一个结构体中的做法被称为 Newtype 模式 (newtype pattern),我们将在“使用 Newtype 模式实现外部 trait”部分中更详细地描述它。我们想要将毫米单位的值加到米单位的值上,并让 Add 的实现正确地进行转换。我们可以为 Millimeters 实现 Add,并将 Meters 作为 Rhs,如示例 20-16 所示。
We have two structs, Millimeters and Meters, holding values in different
units. This thin wrapping of an existing type in another struct is known as the
newtype pattern, which we describe in more detail in the “Implementing
External Traits with the Newtype Pattern” section. We
want to add values in millimeters to values in meters and have the
implementation of Add do the conversion correctly. We can implement Add for
Millimeters with Meters as the Rhs, as shown in Listing 20-16.
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 相加,我们指定 impl Add<Meters> 来设置 Rhs 类型参数的值,而不是使用默认的 Self。
To add Millimeters and Meters, we specify impl Add<Meters> to set the
value of the Rhs type parameter instead of using the default of Self.
你将以两种主要方式使用默认类型参数:
You’ll use default type parameters in two main ways:
-
扩展一个类型而不破坏现有代码
-
允许在大多数用户不需要的特定情况下进行自定义
-
To extend a type without breaking existing code
-
To allow customization in specific cases most users won’t need
标准库的 Add trait 是第二个目的的一个例子:通常情况下,你会将两个相似的类型相加,但 Add trait 提供了除此之外的自定义能力。在 Add trait 定义中使用默认类型参数意味着大多数时候你不需要指定额外的参数。换句话说,不需要一些实现样板,使得 trait 更容易使用。
The standard library’s Add trait is an example of the second purpose:
Usually, you’ll add two like types, but the Add trait provides the ability to
customize beyond that. Using a default type parameter in the Add trait
definition means you don’t have to specify the extra parameter most of the
time. In other words, a bit of implementation boilerplate isn’t needed, making
it easier to use the trait.
第一个目的与第二个目的类似,但方向相反:如果你想向现有的 trait 添加类型参数,你可以给它一个默认值,以便在不破坏现有实现代码的情况下扩展 trait 的功能。
The first purpose is similar to the second but in reverse: If you want to add a type parameter to an existing trait, you can give it a default to allow extension of the functionality of the trait without breaking the existing implementation code.
区分重名的法
Disambiguating Between Identically Named Methods
Rust 中没有任何规定可以防止一个 trait 拥有与另一个 trait 相同名称的方法,Rust 也不会阻止你在一个类型上实现这两个 trait。直接在类型上实现与 trait 中的方法同名的方法也是可能的。
Nothing in Rust prevents a trait from having a method with the same name as another trait’s method, nor does Rust prevent you from implementing both traits on one type. It’s also possible to implement a method directly on the type with the same name as methods from traits.
当调用同名方法时,你需要告诉 Rust 你想使用哪一个。考虑示例 20-17 中的代码,我们定义了两个 trait Pilot 和 Wizard,它们都有一个名为 fly 的方法。然后我们在一个已经实现了名为 fly 方法的类型 Human 上实现这两个 trait。每个 fly 方法的作用都不同。
When calling methods with the same name, you’ll need to tell Rust which one you
want to use. Consider the code in Listing 20-17 where we’ve defined two traits,
Pilot and Wizard, that both have a method called fly. We then implement
both traits on a type Human that already has a method named fly implemented
on it. Each fly method does something different.
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() {}
当我们对 Human 的实例调用 fly 时,编译器默认调用直接在类型上实现的方法,如示例 20-18 所示。
When we call fly on an instance of Human, the compiler defaults to calling
the method that is directly implemented on the type, as shown in Listing 20-18.
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();
}
运行这段代码将打印 *waving arms furiously*,表明 Rust 调用了直接在 Human 上实现的 fly 方法。
Running this code will print *waving arms furiously*, showing that Rust
called the fly method implemented on Human directly.
要从 Pilot trait 或 Wizard trait 调用 fly 方法,我们需要使用更明确的语法来指定我们指的是哪个 fly 方法。示例 20-19 演示了这种语法。
To call the fly methods from either the Pilot trait or the Wizard trait,
we need to use more explicit syntax to specify which fly method we mean.
Listing 20-19 demonstrates this syntax.
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();
}
在方法名之前指定 trait 名称,可以向 Rust 澄清我们想要调用 fly 的哪个实现。我们也可以写 Human::fly(&person),这相当于我们在示例 20-19 中使用的 person.fly(),但在不需要区分的情况下,这样写有点长。
Specifying the trait name before the method name clarifies to Rust which
implementation of fly we want to call. We could also write
Human::fly(&person), which is equivalent to the person.fly() that we used
in Listing 20-19, but this is a bit longer to write if we don’t need to
disambiguate.
运行这段代码将打印以下内容:
Running this code prints the following:
$ 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 的哪种实现。
Because the fly method takes a self parameter, if we had two types that
both implement one trait, Rust could figure out which implementation of a
trait to use based on the type of self.
然而,不是方法的关联函数没有 self 参数。当有多个类型或 trait 定义了具有相同函数名的非方法函数时,除非你使用完全限定语法,否则 Rust 并不总能知道你指的是哪个类型。例如,在示例 20-20 中,我们为一个动物收容所创建了一个 trait,该收容所希望给所有幼犬命名为 Spot。我们创建了一个 Animal trait,其关联的非方法函数为 baby_name。Animal trait 是为结构体 Dog 实现的,在 Dog 上我们也直接提供了一个关联的非方法函数 baby_name。
However, associated functions that are not methods don’t have a self
parameter. When there are multiple types or traits that define non-method
functions with the same function name, Rust doesn’t always know which type you
mean unless you use fully qualified syntax. For example, in Listing 20-20, we
create a trait for an animal shelter that wants to name all baby dogs Spot. We
make an Animal trait with an associated non-method function baby_name. The
Animal trait is implemented for the struct Dog, on which we also provide an
associated non-method function baby_name directly.
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());
}
我们在 Dog 上定义的 baby_name 关联函数中实现了将所有幼犬命名为 Spot 的代码。Dog 类型也实现了 Animal trait,该 trait 描述了所有动物具有的特征。幼狗被称为 puppy,这在 Dog 上的 Animal trait 实现中的 baby_name 函数(与 Animal trait 相关联)中得到了表达。
We implement the code for naming all puppies Spot in the baby_name associated
function that is defined on Dog. The Dog type also implements the trait
Animal, which describes characteristics that all animals have. Baby dogs are
called puppies, and that is expressed in the implementation of the Animal
trait on Dog in the baby_name function associated with the Animal trait.
在 main 中,我们调用 Dog::baby_name 函数,该函数直接调用在 Dog 上定义的关联函数。这段代码打印以下内容:
In main, we call the Dog::baby_name function, which calls the associated
function defined on Dog directly. This code prints the following:
$ 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
这个输出不是我们想要的。我们想要调用作为 Animal trait(我们在 Dog 上实现的)一部分的 baby_name 函数,以便代码打印 A baby dog is called a puppy。我们在示例 20-19 中使用的指定 trait 名称的技术在这里没有帮助;如果我们将 main 更改为示例 20-21 中的代码,我们将得到一个编译错误。
This output isn’t what we wanted. We want to call the baby_name function that
is part of the Animal trait that we implemented on Dog so that the code
prints A baby dog is called a puppy. The technique of specifying the trait
name that we used in Listing 20-19 doesn’t help here; if we change main to
the code in Listing 20-21, we’ll get a compilation error.
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());
}
因为 Animal::baby_name 没有 self 参数,并且可能有其他实现 Animal trait 的类型,Rust 无法弄清楚我们想要 Animal::baby_name 的哪种实现。我们将得到如下编译器错误:
Because Animal::baby_name doesn’t have a self parameter, and there could be
other types that implement the Animal trait, Rust can’t figure out which
implementation of Animal::baby_name we want. We’ll get this compiler error:
$ 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 我们想要使用 Dog 对应的 Animal 实现,而不是某个其他类型对应的 Animal 实现,我们需要使用完全限定语法 (fully qualified syntax)。示例 20-22 演示了如何使用完全限定语法。
To disambiguate and tell Rust that we want to use the implementation of
Animal for Dog as opposed to the implementation of Animal for some other
type, we need to use fully qualified syntax. Listing 20-22 demonstrates how to
use fully qualified syntax.
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());
}
我们在尖括号内向 Rust 提供了一个类型标注,这表明我们想要调用实现在 Dog 上的 Animal trait 中的 baby_name 方法,方法是声明我们希望在这次函数调用中将 Dog 类型视为 Animal。这段代码现在将打印我们想要的内容:
We’re providing Rust with a type annotation within the angle brackets, which
indicates we want to call the baby_name method from the Animal trait as
implemented on Dog by saying that we want to treat the Dog type as an
Animal for this function call. This code will now print what we want:
$ 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
通常,完全限定语法定义如下:
In general, fully qualified syntax is defined as follows:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
对于不是方法的关联函数,将没有 receiver:只有其他参数列表。你可以在调用函数或方法的任何地方使用完全限定语法。但是,如果 Rust 可以从程序中的其他信息推断出任何部分,你可以省略该语法。只有在存在多个使用相同名称的实现且 Rust 需要帮助来确定你想调用哪个实现的情况下,你才需要使用这种更冗长的语法。
For associated functions that aren’t methods, there would not be a receiver:
There would only be the list of other arguments. You could use fully qualified
syntax everywhere that you call functions or methods. However, you’re allowed
to omit any part of this syntax that Rust can figure out from other information
in the program. You only need to use this more verbose syntax in cases where
there are multiple implementations that use the same name and Rust needs help
to identify which implementation you want to call.
使用父 Trait
Using Supertraits
有时你可能会编写一个依赖于另一个 trait 的 trait 定义:为了让一个类型实现第一个 trait,你想要要求该类型同时也实现第二个 trait。你这样做是为了让你的 trait 定义能够使用第二个 trait 的关联项。你的 trait 定义所依赖的这个 trait 被称为你的 trait 的父 trait (supertrait)。
Sometimes you might write a trait definition that depends on another trait: For a type to implement the first trait, you want to require that type to also implement the second trait. You would do this so that your trait definition can make use of the associated items of the second trait. The trait your trait definition is relying on is called a supertrait of your trait.
例如,假设我们想要创建一个具有 outline_print 方法的 OutlinePrint trait,该方法将以星号加框的格式打印给定的值。也就是说,给定一个实现标准库 trait Display 并产生 (x, y) 的 Point 结构体,当我们在一个 x 为 1 且 y 为 3 的 Point 实例上调用 outline_print 时,它应该打印以下内容:
For example, let’s say we want to make an OutlinePrint trait with an
outline_print method that will print a given value formatted so that it’s
framed in asterisks. That is, given a Point struct that implements the
standard library trait Display to result in (x, y), when we call
outline_print on a Point instance that has 1 for x and 3 for y, it
should print the following:
**********
* *
* (1, 3) *
* *
**********
在 outline_print 方法的实现中,我们想要使用 Display trait 的功能。因此,我们需要指定 OutlinePrint trait 仅适用于那些也实现了 Display 并提供 OutlinePrint 所需功能的类型。我们可以在 trait 定义中通过指定 OutlinePrint: Display 来做到这一点。这种技术类似于向 trait 添加 trait bound。示例 20-23 展示了 OutlinePrint trait 的一个实现。
In the implementation of the outline_print method, we want to use the
Display trait’s functionality. Therefore, we need to specify that the
OutlinePrint trait will work only for types that also implement Display and
provide the functionality that OutlinePrint needs. We can do that in the
trait definition by specifying OutlinePrint: Display. This technique is
similar to adding a trait bound to the trait. Listing 20-23 shows an
implementation of the OutlinePrint trait.
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() {}
因为我们已经指定 OutlinePrint 需要 Display trait,所以我们可以使用为任何实现 Display 的类型自动实现的 to_string 函数。如果我们尝试在不添加冒号并在 trait 名称后指定 Display trait 的情况下使用 to_string,我们将得到一个错误,提示在当前作用域内找不到类型 &Self 的名为 to_string 的方法。
Because we’ve specified that OutlinePrint requires the Display trait, we
can use the to_string function that is automatically implemented for any type
that implements Display. If we tried to use to_string without adding a
colon and specifying the Display trait after the trait name, we’d get an
error saying that no method named to_string was found for the type &Self in
the current scope.
让我们看看当我们尝试在未实现 Display 的类型(例如 Point 结构体)上实现 OutlinePrint 时会发生什么:
Let’s see what happens when we try to implement OutlinePrint on a type that
doesn’t implement Display, such as the Point struct:
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 但未实现:
We get an error saying that Display is required but not implemented:
$ 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 {}
| ^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
|
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();
| ^^^^^^^^^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
|
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
要修复此问题,我们在 Point 上实现 Display 并满足 OutlinePrint 所要求的约束,如下所示:
To fix this, we implement Display on Point and satisfy the constraint that
OutlinePrint requires, like so:
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 trait 就会成功编译,我们可以对 Point 实例调用 outline_print 从而在星号轮廓中显示它。
Then, implementing the OutlinePrint trait on Point will compile
successfully, and we can call outline_print on a Point instance to display
it within an outline of asterisks.
使用 Newtype 模式实现外部 Trait
Implementing External Traits with the Newtype Pattern
在第 10 章的“在类型上实现 trait”部分,我们提到了孤儿规则 (orphan rule),该规则规定只有当 trait 或类型(或两者)对于我们的 crate 是本地的时,我们才被允许在类型上实现 trait。可以使用 Newtype 模式来绕过此限制,该模式涉及在元组结构体中创建一个新类型。(我们在第 5 章的“使用元组结构体创建不同类型”部分介绍过元组结构体。)该元组结构体将有一个字段,并成为我们要为其实现 trait 的类型的薄包装。然后,该包装类型对于我们的 crate 是本地的,我们就可以在该包装上实现该 trait。Newtype 是一个起源于 Haskell 编程语言的术语。使用此模式没有运行时性能损失,并且包装类型在编译时会被消除。
In the “Implementing a Trait on a Type” section in Chapter 10, we mentioned the orphan rule that states we’re only allowed to implement a trait on a type if either the trait or the type, or both, are local to our crate. It’s possible to get around this restriction using the newtype pattern, which involves creating a new type in a tuple struct. (We covered tuple structs in the “Creating Different Types with Tuple Structs” section in Chapter 5.) The tuple struct will have one field and be a thin wrapper around the type for which we want to implement a trait. Then, the wrapper type is local to our crate, and we can implement the trait on the wrapper. Newtype is a term that originates from the Haskell programming language. There is no runtime performance penalty for using this pattern, and the wrapper type is elided at compile time.
作为一个例子,假设我们想要在 Vec<T> 上实现 Display,由于 Display trait 和 Vec<T> 类型都定义在我们的 crate 之外,孤儿规则阻止了我们直接这样做。我们可以创建一个持有 Vec<T> 实例的 Wrapper 结构体;然后我们就可以在 Wrapper 上实现 Display 并使用 Vec<T> 的值,如示例 20-24 所示。
As an example, let’s say we want to implement Display on Vec<T>, which the
orphan rule prevents us from doing directly because the Display trait and the
Vec<T> type are defined outside our crate. We can make a Wrapper struct
that holds an instance of Vec<T>; then, we can implement Display on
Wrapper and use the Vec<T> value, as shown in Listing 20-24.
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}");
}
Display 的实现使用 self.0 来访问内部的 Vec<T>,因为 Wrapper 是一个元组结构体,而 Vec<T> 是元组中索引为 0 的项。然后,我们就可以在 Wrapper 上使用 Display trait 的功能了。
The implementation of Display uses self.0 to access the inner Vec<T>
because Wrapper is a tuple struct and Vec<T> is the item at index 0 in the
tuple. Then, we can use the functionality of the Display trait on Wrapper.
使用这种技术的缺点是 Wrapper 是一个新类型,因此它没有它所持有的值的方法。我们将不得不直接在 Wrapper 上实现 Vec<T> 的所有方法,并让这些方法委托给 self.0,这将允许我们将 Wrapper 完全视为 Vec<T>。如果我们想要新类型拥有内部类型所拥有的每一个方法,那么在 Wrapper 上实现 Deref trait 以返回内部类型将是一个解决方案(我们在第 15 章的“像对待普通引用一样对待智能指针”部分讨论过实现 Deref trait)。如果我们不想要 Wrapper 类型拥有内部类型的所有方法——例如,为了限制 Wrapper 类型的一行为——我们将不得不手动仅实现我们想要的方法。
The downside of using this technique is that Wrapper is a new type, so it
doesn’t have the methods of the value it’s holding. We would have to implement
all the methods of Vec<T> directly on Wrapper such that the methods
delegate to self.0, which would allow us to treat Wrapper exactly like a
Vec<T>. If we wanted the new type to have every method the inner type has,
implementing the Deref trait on the Wrapper to return the inner type would
be a solution (we discussed implementing the Deref trait in the “Treating
Smart Pointers Like Regular References”
section in Chapter 15). If we didn’t want the Wrapper type to have all the
methods of the inner type—for example, to restrict the Wrapper type’s
behavior—we would have to implement just the methods we do want manually.
即使不涉及 trait,这种 Newtype 模式也很有用。让我们转换焦点,看看一些与 Rust 类型系统交互的高级方法。
This newtype pattern is also useful even when traits are not involved. Let’s switch focus and look at some advanced ways to interact with Rust’s type system.