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