Updated 'src/advanced_features/traits.md'.

This commit is contained in:
Hector PENG
2026-04-23 15:26:43 +08:00
parent b81bca5162
commit 81263dd133
4 changed files with 106 additions and 103 deletions

View File

@@ -1,23 +1,5 @@
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,
}
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
struct Millimeters(u32);
@@ -32,14 +14,9 @@ impl Add<Meters> for Millimeters {
}
}
fn main() {
assert_eq! (
Point { x: 1, y: 0 } + Point { x: 2, y: 3},
Point { x: 3, y: 3 }
);
fn main () {
let a = Millimeters(100);
let b = Meters(3);
assert_eq! (
Millimeters(50) + Meters(1),
Millimeters(1050)
);
println! ("{:?}", a + b);
}

View File

@@ -0,0 +1,6 @@
[package]
name = "traits_example"
version = "0.1.0"
edition = "2024"
[dependencies]

View File

@@ -0,0 +1,34 @@
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println! ("这是你们的机长在讲话。");
}
}
impl Wizard for Human {
fn fly(&self) {
println! ("上升!");
}
}
impl Human {
fn fly(&self) {
println! ("*疯狂地挥舞双臂*");
}
}
fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();
}

View File

@@ -1,20 +1,17 @@
# 高级特质
在第 10 章 [特质定义共用行为](Ch10_Generic_Types_Traits_and_Lifetimes.md#特质定义共用行为) 小节中,咱们曾首先涉及到特质,但咱们不曾讨论更为高级的那些细节。现在咱们对 Rust 有了更了解,们就可以深入本质get into the nitty-gritty
我们在第 10 章 [特质定义共用行为](../generic_types_traits_and_lifetimes/traits.md) 小节中首次介绍了特质,但我们未曾讨论一些更高级的细节。现在咱们对 Rust 有了更深入的了解,们就可以深入探讨其中的细节了
## 使用关联类型指定出特质定义中的一些占位性类型
## 通过关联类型定义特质
**Specifying placeholder types in trait definitions with associated types**
所谓 *关联类型*,将类型占位符与特质连接,使得特质方法的定义可以在其签名中使用这些占位符类型。特质的实现者将针对特定实现,指定要使用的具体类型以取代占位符类型。这样,我们就可以无需在特质被实现之前确切地知道类型是什么下,定义一个使用某些类型的特质。
在本章中,我们提到大多数高级特性很少被用到。关联类型处于中间位置:他们的使用频率虽低于本书其余部分中介绍的特性,但比这一章中讨论的其他特性更为常用。
*关联类型* 将类型占位符与特质加以结合,从而那些特质方法的定义,就可以在他们的签名中,使用这些占位符类型。特质的实现者,将为其特定实现,指明占位符类型所要使用的具体类型。如此一来,咱们便可以在特质被实现之前,无需准确获悉特质用到的类型下,定义出用到这些类型的特质
在本章中,咱们已经介绍了绝大多数极少需要用到的高级特性。而关联类型则是位于这些高级特性中部的一种:其相较本书其余部分降到的那些特性,用得尤其少见,但相较这一章中讨论到的其他特性,则其要更常用一些。
带有关联类型特质的一个示例,便是标准库所提供的 `Iterator` 特质。其中的关联类型名为 `Item`,且代表着实现了这个 `Iterator` 特质的类型所迭代的那些值的类型。`Iterator` 特质的定义如下清单 19-12 中所示。
一个带有关联类型特质的示例是,标准库提供的 `Iterator` 特质。其中的关联类型名为 `Item`,代表着实现 `Iterator` 特质的类型正在迭代的值的类型。`Iterator` 特质的定义如下清单 20-13 中所示
<a name="listing_20-13"></a>
```rust
pub trait Iterator {
type Item;
@@ -23,11 +20,11 @@ pub trait Iterator {
}
```
*清单 19-12:有着关联类型 `Item` 的 `Iterator` 特质的定义*
**清单 20-13**:有着关联类型 `Item``Iterator` 特质的定义
其中的类型 `Item` 便是个占位符,而那个 `next` 方法的定义,则显示其将返回类型类型`Option<Self::Item>` 的值。`Iterator` 的实现者将指 `Item` 的具体类型,同时 `next` 方法将返回包含那个具体类型值的一个 `Option`
类型 `Item` 是个占位符,而 `next` 方法的定义表明他将返回类型为 `Option<Self::Item>` 的值。`Iterator` 特质的实现者将指 `Item` 的具体类型, `next` 方法将返回包含一个包含该具体类型值的 `Option`
从泛型允许们在不指函数可处理何种类型下,而定义出某个函数上看,关联类型可能看起来是个与泛型类似类似的概念。为检视这两个概念的不同,咱们将看看在指定了 `Item` 类型为 `u32` 的一个名为 `Counter` 的类型`Iterator` 实现
关联类型可能看起来是个与泛型类似的概念,因为后者允许们在不指函数可处理哪些类型下定义函数。为了探讨这两个概念之间的区别,我们将看看对名为 `Counter` 的类型的一种 `Iterator` 特质的实现,该实现指定 `Item` 类型为 `u32`
```rust
impl Iterator for Counter {
@@ -37,7 +34,7 @@ impl Iterator for Counter {
// --跳过代码--
```
这种语法似乎可与泛型的那种语法相比。那么为何没有只使用泛型定义 `Iterator`,如下清单 19-13 中所示的那样呢?
这种语法看起来与泛型相当。那么,为什么不直接如下清单 20-14 中所示那样,使用使用泛型定义 `Iterator` 呢?
```rust
pub trait Iterator<T> {
@@ -45,32 +42,26 @@ pub trait Iterator<T> {
}
```
*清单 19-13:使用泛型的一种 `Iterator` 特质的定义假设*
**清单 20-14**:使用泛型`Iterator` 特质的假设定义
不同之处在于,如同清单 19-13使用泛型时,咱们必须注解每个实现中的那些类型;由于们还可以实现 `Iterfator<String> for Counter` 或任何其他类型,们可以有对 `Counter` 多个 `Iterator` 实现。也就是说,特质有着泛型参数时,他可以对某个类型被实现多次,每次都修改泛型参数的具体类型。当 `Counter` 使用 `next` 方法时,咱们就将不得不提供类型注解,来表明咱们想要使用哪个 `Iterator` 实现。
区别在于,当使用泛型时,如清单 20-14那样,我们必须每个实现中注解类型;由于们还可以针对 `Counter` 实现 `Iterfator<String>` 或任何其他类型,因此我们可以 `Counter` 提供多个 `Iterator` 实现。换句话说,特质包含泛型参数时,他可以针对一个类型被实现多次,每次都修改泛型类型参数的具体类型。当我们对 `Counter` 使用 `next` 方法时,我们将必须提供类型注解,来表明我们打算使用的 `Iterator` 实现。
在关联类型下,由于我们无法一个类型多次实现某个特质,因此就无需注解类型。在上面有着用到关联类型定义的清单 9-12 中,由于只能有一个 `impl Iterator for Counter`,所以咱们就只能就`Item` 为何选择一次。咱们不必在 `Counter` 调用 `next` 的每个地方,指定咱们所要的是`u32` 值的迭代器。
在关联类型下,我们不需要类型注解,因为我们无法对同一个类型多次实现某个特质。在有着使用关联类型定义的清单 20-13 中,我们只能选择一次 `Item` 的类型,因为只能有一个 `impl Iterator for Counter`。我们不必在 `Counter` 调用 `next` 的每个地方,指定我们想要一`u32` 值的迭代器。
关联类型也成为了特质合约的一部分:特质的实现者必须提供一种类型,来代替关联类型的占位符。关联类型通常有着一个描述其使用方式的名字,并且在 API 文档中记录关联类型是一种很好的做法。
关联类型还成了特质合约的一部分:特质的实现着必须提供一种类型,来顶替那个关联类型占位符。关联类型通常会有个描述该类型将被如何使用的名字,而在 API 文档中对关联类型编写文档,则是良好的做法。
## 使用默认泛型类型参数和运算符重载
> **译注**:请参考 [Difference Between Method Overloading and Method Overriding in Java](https://www.geeksforgeeks.org/difference-between-method-overloading-and-method-overriding-in-java/) 了解 Java 中的重载与重写的区别。
## 默认泛型参数与运算符的重载
在泛型类型参数时,我们可以为泛型类型指定一种默认的具体类型。当默认类型有效时,这样做消除了特质的实现者指定具体类型的需要。在声明泛型类型时,咱们可以通过 `<PlaceholderType=ConcreteType>` 语法指定默认类型。
**Default Generic Type Parameters and Operator Overloading**
> **注**:请参考 [Difference Between Method Overloading and Method Overriding in Java](https://www.geeksforgeeks.org/difference-between-method-overloading-and-method-overriding-in-java/) 了解 Java 中的重载与重写区别。
在咱们用到泛型参数时,咱们可以给泛型指定默认具体类型。在所指定的默认类型就有效时,这样做消除了实现者指定具体类型的需求。在声明泛型时使用 `<PlaceholderType=ConcreteType>` 语法,指定出默认类型。
这种技巧有用处情形的一个了不起示例,便是 *运算符重载operator overloading*,咱们可以其在某些情形下,定制某个运算符(比如 `+`)的行为。
Rust 不允许咱们创建自己的运算符,或重载任意运算符。但咱们可以通过实现与运算符相关的特质,而重载那些运算及于 `std::ops` 中所列出的相应特质。比如,在下面清单 19-14 中,咱们就将 `+` 运算符过载为把两个 `Point` 实例加在一起。咱们是通过在 `Point` 结构体上实现 `Add` 特质完成这一点的。
这种技巧有用的一个很好的示例是 *运算符重载*,其中咱们可以在特定情形下自定义运算符(比如 `+`)的行为。
Rust 不允许咱们创建自己的运算符,或重载任意运算符。但咱们可以通过实现与运算符相关的特质,来重载 `std::ops` 中列出的操作和对应的特质。例如,在下面清单 20-15 中,我们重载了 `+` 运算符,以将两个 `Point` 实例相加。我们通过对 `Point` 结构体实现 `Add` 特质来实现这点。
<a name="listing_20-15"></a>
文件名:`src/main.rs`
```rust
@@ -101,12 +92,11 @@ fn main() {
}
```
*清单 19-14:实现 `Add` 特质来为 `Point` 实例载 `+` 运算符*
**清单 20-15**:实现 `Add` 特质,以针对`Point` 实例`+` 运算符
这里的 `add` 方法将两个 `Point` 实例的 `x`两个实例的 `y` 值相加,而创建一个新的 `Point`这里的 `Add` 特质有着一个确定自其中的 `add` 方法返回类型,名为的 `Output` 关联类型。
此代码中的默认泛型,是在 `Add` 特质里。以下便是其定义:
其中 `add` 方法将两个 `Point` 实例的 `x`两个实例的 `y` 值相加,而创建一个新的 `Point``Add` 特质有个名为 `Output` 的关联类型,确定从 `add` 方法返回的类型。
这段代码中的默认泛型类型位于 `Add` 特质内部。以下是其定义:
```rust
trait Add<Rhs=Self> {
@@ -116,15 +106,16 @@ trait Add<Rhs=Self> {
}
```
代码看起来应相当熟悉:有着一个方法关联类型的特质。其中新的部分 `Rhs=Self`:这种语法叫做 *默认类型参数default type parameters*。其中 `Rhs` 泛型参数(`right hand side` 的缩写),定义了 `add` 方法中 `rhs` 参数的类型,当咱们实现这个 `Add` 特质,而没有指明 `Rhs` 类型时,`Rhs` 的类型将默认为 `Self`其将是咱们在其上实现 `Add` 的类型。
这段代码看起来应熟悉:一个包含一个方法关联类型的特质。新的部分 `Rhs=Self`:这种语法叫做 *默认类型参数*。其中 `Rhs` 泛型类型参数(`right hand side` 的缩写),定义了 `add` 方法中 `rhs` 参数的类型。当我们在实现 `Add` 特质时没有为 `Rhs` 指定具体类型时,`Rhs` 的类型将默认为 `Self`即我们正在对其实现 `Add` 的类型。
当咱们为 `Point` 实现 `Add` 时,由于咱们打算把两个 `Point` 实例相加,因此而使用了 `Rhs` 的默认值。接下来看看,其中咱们打算定制那个 `Rhs` 而非使用其默认值的一个 `Add` 实现示例。
我们有着两个结构体,`Millimeters``Meters`,保存不同单位的值。这种在另一类型中对现有类型的简单封装,成为 *新类型模式newtype pattern*,我们会在 [通过新型模式实现外部特质](#通过新型模式实现外部特质) 小节对此进行更详细的说明。我们打算将以毫米为单位的值,与以米为单位的值相加,并要让 `Add` 的实现正确地进行单位转换。我们通过将 `Meters` 作为 `Rhs`,对 `Millimeters` 实现 `Add`,如下清单 20-16 中所示。
咱们有着两个结构体,`Millimeters``Meters`保存着不同单位的一些值。这种将某个既有类型封装在另一结构体的瘦封装thin wrapping就叫做 *新类型模式newtype pattern*,在后面的 [“使用新型模式在外部类型上实现外部特质”](#使用新型模式在外层类型上实现外层的特质) 小节,咱们会对其进行更深入讨论。咱们打算把毫米值与以米计数的值相加,并要让 `Add` 的实现,正确完成单位转换。咱们可在将 `Meters` 作为 `Rhs` 下,对 `Millimeters` 实现 `Add`,如下清单 19-15 中所示。
<a name="listing_20-16"></a>
```rust
use std::ops::Add;
#[derive(Debug, Copy, Clone, PartialEq)]
struct Millimeters(u32);
@@ -140,30 +131,27 @@ impl Add<Meters> for Millimeters {
}
```
*清单 19-15`Millimeters` 实现 `Add` 特质,以将 `Millimeters` 与 `Meters` 相加*
**清单 20-16**:对 `Millimeters` 实现 `Add` 特质,以将 `Millimeters``Meters` 相加
为了将 `Millimeters` `Meters` 相加,们指明了 `impl Add<Meters>` 设置那个 `Rhs` 类型参数,而非使用其默认的 `Self`
为了将 `Millimeters` `Meters` 相加,们指 `impl Add<Meters>` 设置 `Rhs` 类型参数的值,而不是使用其默认的 `Self`
咱们将以如下两种主要方式,使用默认类型参数:
咱们将以如下两种主要方式,使用默认类型参数:
- 在不破坏有代码下,扩展某个类型;
- 为实现绝大多数不会需要的特定情形下的定制to allow customization in specific cases most users won't need
1. 在不破坏有代码的情况下,扩展某个类型;
2. 允许在大多数用户不需要的特定情形下进行自定义
标准库的 `Add` 特质就是第二种用途的一个示例:通常,咱们将把两个相似的类型相加,但 `Add` 特质提供了超越这种情况的自定义能力。在 `Add` 特质中使用默认类型意味着,在大多数时候咱们不必指定额外的参数。换句话说,部分实现样板代码不再需要,从而使得更容易使用该特质。
第一个用途与第二种类似,但方向相反:当咱们打算添加类型参数到某个现有特质时,咱们可以给予他一个默认类型参数,从而在不破坏现有实现代码的情况下,扩展该特质的功能。
标准库的 `Add` 特质,便是第二种目的的一个示例:通常,咱们将把两个相似类型相加,但 `Add` 特质提供了定制超出那种情况的能力。在 `Add` 特质中使用默认类型,就意味着咱们不必在多数时候指定额外的参数。换句话说,并不需要一点点的实现样板,从而令到使用这个特质更为容易。
## 消除同名方法之间的歧义
第一个目的与第二个类似,不过是反过来的:在咱们打算将类型参数添加到某个既有特质时,就可以给到其一个默认值,从而在不破坏既有那些实现代码下,实现特质功能的扩展
Rust 并未禁止一个特质有着与另一特质的方法同名的方法,也不会阻止咱们对同一个类型实现这两个特质。咱们还可以对类型实现特质中方法同名的方法
在调用名字相同的方法时,咱们需要告诉 Rust 希望使用哪个。请考虑下面清单 20-17 中的代码,其中我们定义了两个特质 `Pilot``Wizard`,他们都包含一个名为 `fly` 的方法。然后,我们对 `Human` 类型实现了这两个特质,该类型本身已经实现了一个名为 `fly` 的方法。每个 `fly` 方法都执行不同的操作。
## 用于消除歧义的完全合格语法:以同一名字调用方法
**Fully Qualified Syntax for Disambiguation: Calling Methods with the Same Name**
Rust 中没有什么可以阻止某个特质有着与另一特质的方法同样名字的方法Rust 也不会阻止咱们在一个类型上实现这两种特质。至于直接在类型上,以来自不同特质方法的同样名字实现方法,也是可行的。
在以同一名字调用这些方法时,咱们将需要告诉 Rust 打算使用哪一个。设想下面清单 19-16 中,定义了两个特质,`Pilot``Wizard`,两个特质都有一个叫做 `fly` 的代码。咱们随后在已在其上实现了一个名为 `fly` 方法的类型 `Human` 上,实现了这两个特质。每个 `fly` 都完成不同的事情。
<a name="listing_20-17"></a>
```rust
trait Pilot {
fn fly(&self);
@@ -177,27 +165,28 @@ struct Human;
impl Pilot for Human {
fn fly(&self) {
println! ("机长在此发言");
println! ("这是你们的机长在讲话");
}
}
impl Wizard for Human {
fn fly(&self) {
println! ("飞起来");
println! ("上升");
}
}
impl Human {
fn fly(&self) {
println! ("*愤怒地挥双臂*");
println! ("*疯狂地挥双臂*");
}
}
```
*清单 19-16:两个被定义作有 `fly` 方法的特质并都在 `Human` 类型上被实现,在 `Human` 上直接实现了一个 `fly` 方法*
**清单 20-17**:两个特质被定义为有着 `fly` 方法,并得以对 `Human` 类型实现,以及一个直接`Human`实现的 `fly` 方法
咱们在 `Human` 实例调用 `fly` 时,编译器默认调用直接在该类型上实现的那个方法,如下清单 19-17 中所示。
我们对 `Human` 实例调用 `fly` 时,编译器默认调用直接在该类型上实现的方法,如下清单 20-18 中所示。
<a name="listing_20-18"></a>
```rust
fn main() {
let person = Human;
@@ -205,12 +194,13 @@ fn main() {
}
```
*清单 19-17调用 `Human` 实例上的 `fly`*
**清单 20-18**:对 `Human` 实例调用 `fly`
运行代码将打印 `*愤怒地挥双臂*`显示 Rust 调用了直接在 `Human`实现的那个 `fly` 方法。
运行这段代码将打印 `*疯狂地挥双臂*`表明 Rust 调用了直接定义`Human` `fly` 方法。
为了调用 `Pilot``Wizard` 特质`fly` 方法,们需要使用更为显式的语法,来指我们指的是`fly` 方法。下面清单 19-18 对此语法进行了演示
为了调用 `Pilot``Wizard` 特质`fly` 方法,们需要使用更明确的语法,来指我们指的是`fly` 方法。下面清单 20-19 演示了这种语法
<a name="listing_20-19"></a>
文件名:`src/main.rs`
```rust
@@ -222,22 +212,20 @@ fn main() {
}
```
*清单 19-18指明咱们打算调用哪个特质的 `fly` 方法*
**清单 20-19**:指定我们打算调用哪个特质的 `fly` 方法
在方法名字前指定特质名字,可以向 Rust 澄清我们打算调用的 `fly` 的实现。我们原本可以写下 `Human::fly(&person)`,这等同于我们在清单 20-19 中使用的 `person.fly()`,但若我们不需要消除歧义,那么这种写法稍微冗长一些。
在方法名字前指明特质名字,就向 Rust 澄清了咱们打算调用 `fly` 的哪个实现。咱们本来也可以写下 `Human::fly(&person)`,这与咱们曾在清单 19-18 中所使用的 `person.fly()` 等级,但若咱们无需消除歧义,这样写起来就些许有些长了。
运行此代码会打印以下输出:
运行这段代码会打印以下内容:
```console
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/disambiguation`
机长在此发言。
飞起来!
*愤怒地挥动双臂*
Compiling traits_example v0.1.0 (/home/hector/rust-lang-zh_CN/projects/traits_example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.06s
Running `target/debug/traits_example`
这是你们的机长在讲话。
上升!
*疯狂地挥舞双臂*
```
由于 `fly` 方法取了一个 `self` 参数,那么当咱们有着实现了一个 *特质* 的两个 *类型*Rust 就可以根据 `self` 的类型,找出要使用特质的哪个实现。
@@ -438,9 +426,7 @@ impl fmt::Display for Point {
随后在 `Point` 上实现 `OutlinePrint` 就将成功编译,而咱们就可以在 `Point` 实例上调用 `outline_print` 来将其实现在星号轮廓里了。
## 使用新型模式在外层类型上实现外层的特质
**Using the Newtype Pattern to Implement External Traits on External Types**
## 通过新型模式实现外特质
第 10 章中的 [“在类型上实现特质”](Ch10_Generic_Types_Traits_and_Lifetimes.md#在类型上实现某个特质) 小节咱们曾提到指明只有当特质或类型二者之一属于代码本地的时咱们才被允许在类型上实现特质的孤儿规则the orphan rule。而使用涉及到在元组结构体中创建出一个新类型的 *新型模式newtype pattern*,那么绕过这种限制便是可行的了。(咱们曾在第 5 章的 [“使用不带命名字段的元组结构体来创建不同类型”](Ch05_Using_Structs_to_Structure_Related_Data.md#使用不带命名字段的元组结构体来创建不同类型) 小节谈到过元组结构体这种元组结构体讲有一个字段且将是围绕咱们要实现某个特质的类型的一个瘦封装a thin wrapper。随后这个封装类型便是咱们代码箱的本地类型了而咱们就可以在这个封装上实现那个特质了。所谓 *新型newtype*,是源自 Haskell 编程语言的一个术语。使用这种模式没有运行时性能代码,同时那个封装类型在编译时会被略去。