高级特征
我们首先在“特征:定义共享 行为“部分 10 中,但我们没有讨论更高级的细节。现在您知道更多 关于 Rust,我们可以深入了解细节。
使用关联类型在 Trait Definitions 中指定占位符类型
关联类型将类型占位符与 trait 连接起来,以便 trait 方法定义可以在其签名中使用这些占位符类型。这 trait 的 implementor 将指定要使用的具体类型,而不是 placeholder 类型。这样,我们就可以定义一个 trait 使用某些类型,但不需要确切知道这些类型是什么 ,直到实现 trait。
我们在本章中描述了大多数高级功能,因为很少 需要。关联类型介于两者之间:它们很少使用 比本书其余部分解释的功能更常见,但比许多 本章中讨论的其他功能。
具有关联类型的 trait 的一个示例是 trait 中的
standard 库提供。关联的类型被命名并位于
对于值的 type ,实现 trait 的类型为
迭代。trait 的定义如 清单 所示
19-12.Iterator
Item
Iterator
Iterator
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
该类型是一个占位符,该方法的定义显示
它将返回 类型的值。trait 的实现者将为 指定具体类型,该方法将返回一个包含该具体类型的值。Item
next
Option<Self::Item>
Iterator
Item
next
Option
关联类型可能看起来与泛型的概念类似,因为
后者允许我们定义一个函数,而无需指定它可以是什么类型
处理。为了检查这两个概念之间的区别,我们将查看一个
在名为 trait 的类型上实现,该类型指定
类型为:Iterator
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
}
}
}
此语法似乎与泛型的语法相当。那么为什么不直接用泛型定义 trait,如示例 19-13 所示呢?Iterator
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
区别在于,当使用泛型时,如示例 19-13 所示,我们必须
注释每个 implementation中的类型;因为我们也可以实现或任何其他类型,所以我们可以有多个
for 的实现。换句话说,当特征具有
generic 参数,它可以多次为一个类型实现,更改
每次泛型类型参数的具体类型。当我们使用 on 方法时,我们必须向
指示我们要使用的 实现。Iterator<String> for Counter
Iterator
Counter
next
Counter
Iterator
使用关联类型,我们不需要注释类型,因为我们不能
多次在一个类型上实现一个 trait。在示例 19-12 中,使用
定义,我们只能选择一次 的类型,因为只能有一个 。
我们不必指定我们想要一个到处都是值的迭代器
我们呼吁 .Item
impl Iterator for Counter
u32
next
Counter
关联类型也成为 trait 契约的一部分:的 trait 必须提供一个 type 来代替关联的 type placeholder。 关联类型通常具有描述如何使用类型的名称。 在 API 文档中记录关联的类型是一种很好的做法。
默认泛型类型参数和运算符重载
当我们使用泛型类型参数时,我们可以为
泛型类型。这消除了 trait 的实现者对
如果默认类型有效,请指定具体类型。指定默认类型
当使用语法声明泛型类型时。<PlaceholderType=ConcreteType>
此技术有用的一个很好的示例是使用运算符
重载,其中自定义运算符(如 )
在特定情况下。+
Rust 不允许你创建自己的运算符或重载任意
运营商。但是,您可以重载列出的作和相应的特征
in 通过实施与 Operator 关联的特征。为
例如,在示例 19-14 中,我们重载了 Operator 以将两个实例添加在一起。我们通过在结构体上实现 trait 来实现这一点:std::ops
+
Point
Add
Point
文件名: 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 } ); }
该方法将两个实例的值和两个实例的值相加,以创建新的 .该 trait 具有
associated type 命名,用于确定从方法返回的类型。add
x
Point
y
Point
Point
Add
Output
add
此代码中的默认泛型类型位于 trait 中。这是它的
定义:Add
#![allow(unused)] fn main() { trait Add<Rhs=Self> { type Output; fn add(self, rhs: Rhs) -> Self::Output; } }
这段代码应该看起来大致很熟悉:一个具有一个方法的 trait 和一个
associated 类型。新部分是 :这个语法叫做 default
类型参数。泛型类型参数(“right hand” 的缩写
side“) 定义方法中参数的类型。如果我们不这样做
指定一个具体类型,当我们实现 trait 时,类型
of 将默认为 ,这将是我们正在实现的类型。Rhs=Self
Rhs
rhs
add
Rhs
Add
Rhs
Self
Add
当我们实现 for 时,我们使用了默认的 for,因为我们
想要添加两个实例。让我们看一个实现
我们想要自定义类型的 trait,而不是使用
违约。Add
Point
Rhs
Point
Add
Rhs
我们有两个结构体,而 ,将值保存在不同的
单位。将现有类型放在另一个结构体中的这种薄包装称为 newtype 模式,我们在“使用 newtype
Pattern to Implement External traits on External Types“部分。我们想将以毫米为单位的值与以米为单位的值相加,并得到
正确执行转换的实现。我们可以将 for 实现为 ,如示例 19-15 所示。Millimeters
Meters
Add
Add
Millimeters
Meters
Rhs
文件名: 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))
}
}
要添加 和 ,我们指定将
值,而不是使用默认值 .Millimeters
Meters
impl Add<Meters>
Rhs
Self
您将以两种主要方式使用默认类型参数:
- 在不破坏现有代码的情况下扩展类型
- 为了允许在特定情况下进行自定义,大多数用户不需要
标准库的 trait 是第二个目的的一个例子:
通常,你会添加两个 like 类型,但 trait 提供了
除此之外进行定制。在 trait 中使用默认类型参数
定义意味着您不必指定大多数
时间。换句话说,不需要一些实现样板,使
使用 trait 更容易。Add
Add
Add
第一个目的与第二个目的类似,但方向相反:如果要添加 type 参数添加到现有 trait 中,您可以为其指定默认值以允许 在不破坏现有 implementation code 的 implementation code 中。
消除歧义的完全限定语法:调用具有相同名称的方法
Rust 中没有任何内容可以阻止 trait 具有与 another trait 的方法,Rust 也不会阻止你实现这两个 trait 在一种类型上。也可以使用 与 traits 中的方法同名。
当调用具有相同名称的方法时,你需要告诉 Rust 你是哪一个
想要使用。考虑示例 19-16 中的代码,其中我们定义了两个 trait,和 ,它们都有一个名为 .然后,我们实施
类型上的两个特征都已具有名为 implemented 的方法
在上面。每种方法的作用都不同。Pilot
Wizard
fly
Human
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-17 所示。fly
Human
文件名: 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(); }
运行此代码将打印 ,显示 Rust
调用了 直接实现的方法。*waving arms furiously*
fly
Human
要从 trait 或 trait 调用方法,
我们需要使用更明确的语法来指定我们指的是哪种方法。
示例 19-18 演示了此语法。fly
Pilot
Wizard
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() { let person = Human; Pilot::fly(&person); Wizard::fly(&person); person.fly(); }
在方法名称之前指定 trait name 会向 Rust 阐明哪个
we want to call 的实现。我们也可以编写 ,它相当于我们使用的
在示例 19-18 中,但如果我们不需要的话,写起来会有点长
消除歧义。fly
Human::fly(&person)
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*
因为该方法需要一个参数,所以如果我们有两个类型,
都实现了一个 trait,Rust 可以找出
根据 的类型使用 的 trait。fly
self
self
但是,不是方法的关联函数没有参数。当有多个类型或特征定义非方法
函数具有相同的函数名称,Rust 并不总是知道你是哪种类型
表示,除非你使用完全限定的语法。例如,在示例 19-19 中,我们
为想要将所有婴儿狗命名为 Spot 的动物收容所创建一个特征。
我们使用关联的非方法函数 创建 trait 。
该 trait 是为 struct 实现的,我们还在该结构体上
直接提供关联的非方法函数。self
Animal
baby_name
Animal
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()); }
我们在关联的
在 上定义的函数。该类型还实现了 trait ,它描述了所有动物都具有的特征。婴儿犬是
称为 puppies,这在与 trait 关联的函数中 trait on 的实现中表示。baby_name
Dog
Dog
Animal
Animal
Dog
baby_name
Animal
在 中,我们调用函数,该函数调用关联的
函数。此代码打印以下内容: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
这个输出不是我们想要的。我们想调用的函数
是我们实现的 trait 的一部分,因此代码会打印 。指定 trait 名称的技术
我们在示例 19-18 中使用的在这里没有帮助;如果我们将
示例 19-20,我们将得到一个编译错误。baby_name
Animal
Dog
A baby dog is called a puppy
main
文件名: 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());
}
因为没有参数,并且可能有
其他实现 trait 的类型,Rust 无法弄清楚是哪个
实现 We Want。我们将收到这个编译器错误:Animal::baby_name
self
Animal
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 我们想使用 for 的实现,而不是其他 for 的实现
type 时,我们需要使用完全限定的语法。示例 19-21 演示了如何
使用完全限定的语法。Animal
Dog
Animal
文件名: 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()); }
我们在尖括号内为 Rust 提供了一个类型注释,该
表示我们想从 trait 中调用 method as
通过表示我们想将类型视为 for this function 调用来实现 on。这段代码现在将打印我们想要的内容:baby_name
Animal
Dog
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, ...);
对于不是方法的关联函数,不会有 :
只有其他参数的列表。您可以使用 Fully qualified
语法。但是,您可以
省略 Rust 可以从其他信息中找出的语法的任何部分
在程序中。您只需在以下情况下使用这种更详细的语法
有多个实现使用相同的名称,Rust 需要帮助
来确定要调用的实现。receiver
使用 supertrait 要求一个特征在另一个特征中的功能
有时,您可能会编写一个依赖于另一个 trait 的 trait 定义: 对于要实现第一个 trait 的类型,您希望要求该类型也 实现第二个 trait。您这样做是为了让您的特征定义可以 利用第二个特征的关联项。特征 您的特征 定义 is 依赖于 称为 你的 trait 的 supertrait。
例如,假设我们想使用一种方法创建一个 trait,该方法将打印一个格式化的给定值,以便它是
以星号装裱。也就是说,给定一个实现
标准库 trait 生成 ,当我们调用具有 for 和 for 的实例时,它会
应打印以下内容:OutlinePrint
outline_print
Point
Display
(x, y)
outline_print
Point
1
x
3
y
**********
* *
* (1, 3) *
* *
**********
在该方法的实现中,我们希望使用 trait 的功能。因此,我们需要指定 trait 仅适用于同时实现和
提供所需的功能。我们可以在
trait 定义。该技术是
类似于添加绑定到 trait 的 trait。示例 19-22 显示了一个
trait 的实现。outline_print
Display
OutlinePrint
Display
OutlinePrint
OutlinePrint: Display
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)); } } fn main() {}
因为我们指定了需要 trait 的 trait,所以我们
可以使用为任何类型的自动实现的函数
实现 .如果我们尝试在不添加
冒号并在 trait name 后指定 trait,我们将得到一个
错误地指出没有找到 in 中的类型的 method named
当前范围。OutlinePrint
Display
to_string
Display
to_string
Display
to_string
&Self
让我们看看当我们尝试在
没有实现,例如 struct:OutlinePrint
Display
Point
文件名: 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();
}
我们收到一个错误,指出 that is required but not implemented: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
为了解决这个问题,我们实现 on 并满足所需的 constraint,如下所示:Display
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(); }
然后实现 trait on 将编译
成功,我们可以调用一个实例来显示
它在星号的轮廓内。OutlinePrint
Point
outline_print
Point
使用 newtype 模式在外部类型上实现 external trait
在第 10 章的 “在 Type“部分,我们提到了 orphan 规则,它规定我们只允许在类型上实现 trait,如果 trait 或 type 都是我们 crate 的本地。有可能获得 使用 newType 模式绕过此限制,这涉及创建一个 new 类型。(我们在“使用 Tuple 没有命名字段的结构体来创建不同类型的“部分。元组结构将有一个字段,并且是一个 thin 包装器。然后,包装器 type 是我们的 crate 的本地类型,我们可以在 wrapper 上实现 trait。Newtype 是一个源自 Haskell 编程语言的术语。 使用此模式不会对运行时性能造成影响,并且包装器 type 在编译时被省略。
例如,假设我们想在 上实现 ,其中
孤儿规则阻止我们直接执行作,因为 trait 和 type 是在 crate 外部定义的。我们可以创建一个 struct
它保存 ;然后我们就可以实现 on 并使用这个值,如示例 19-23 所示。Display
Vec<T>
Display
Vec<T>
Wrapper
Vec<T>
Display
Wrapper
Vec<T>
文件名: 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}"); }
使用 implements 访问内部 ,
因为 是一个元组结构,并且是
元。然后,我们可以在 上使用 trait 的功能。Display
self.0
Vec<T>
Wrapper
Vec<T>
Display
Wrapper
使用这种技术的缺点是它是一种新型,因此它
没有它所持有的价值的方法。我们必须实施
的所有方法都直接对 on 使得
delegate 传递给 ,这将允许我们完全像 .如果我们希望新类型具有内部类型所具有的所有方法,
实现 trait(在“Treat Smart”的第 15 章中讨论
指针,如具有 Deref
特征的常规引用“部分)返回
内部类型将是一个解决方案。如果我们不希望类型具有
inner 类型的所有方法,例如,限制类型的
行为 — 我们只需要手动实现我们想要的方法。Wrapper
Vec<T>
Wrapper
self.0
Wrapper
Vec<T>
Deref
Wrapper
Wrapper
Wrapper
即使不涉及 trait,这种 newtype 模式也很有用。让我们 切换焦点并查看一些与 Rust 的类型系统交互的高级方法。
本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准