From 50d8b516897231c195d2c39cbf3bf1b7d0f055c6 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Sun, 9 Dec 2018 01:21:01 +0800 Subject: [PATCH] check to ch19-03 --- src/ch19-00-advanced-features.md | 11 +-- src/ch19-01-unsafe-rust.md | 130 ++++++++++++------------------ src/ch19-02-advanced-lifetimes.md | 101 ++++++++++++----------- src/ch19-03-advanced-traits.md | 72 +++++------------ 4 files changed, 130 insertions(+), 184 deletions(-) diff --git a/src/ch19-00-advanced-features.md b/src/ch19-00-advanced-features.md index d74ffae..d01f24d 100644 --- a/src/ch19-00-advanced-features.md +++ b/src/ch19-00-advanced-features.md @@ -1,17 +1,18 @@ # 高级特征 -> [ch19-00-advanced-features.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch19-00-advanced-features.md) +> [ch19-00-advanced-features.md](https://github.com/rust-lang/book/blob/master/src/ch19-00-advanced-features.md) >
-> commit 9f03d42e2f47871fe813496b9324548ef4457862 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -我们已经走得很远了!现在我们已经学习了 99% 的编写 Rust 时需要了解的内容。在第二十章开始另一个新项目之前,让我们聊聊你可能会遇到的最后 1% 的内容。当你不经意间遇到未知的内容时请随意将本章作为参考;这里将要学习的特征在某些非常特定的情况下很有用处。我们并不希望忽略这些特性,但是你会发现很少会碰到它们。 +现在我们已经学习了 Rust 编程语言中最常用的部分。在第二十章开始另一个新项目之前,让我们聊聊一些总有一天你会遇上的部分内容。你可以将本章作为不经意间遇到未知的内容时的参考。本章将要学习的功能在一些非常特定的场景下很有用处。虽然很少会碰到它们,我们希望确保你了解 Rust 提供的所有功能。 本章将涉及如下内容: -* 不安全 Rust:用于当需要舍弃 Rust 的某些保证并由你自己负责维持这些保证 +* 不安全 Rust:用于当需要舍弃 Rust 的某些保证并负责手动维持这些保证 * 高级生命周期:用于复杂生命周期情况的语法 * 高级 trait:与 trait 相关的关联类型,默认类型参数,完全限定语法(fully qualified syntax),超(父)trait(supertraits)和 newtype 模式 -* 高级类型:关于 newtype 模式的更多内容,类型别名,“never” 类型和动态大小类型 +* 高级类型:关于 newtype 模式的更多内容,类型别名,never 类型和动态大小类型 * 高级函数和闭包:函数指针和返回闭包 +* 宏:定义在编译时定义更多更多代码的方式 对所有人而言,这都是一个介绍 Rust 迷人特性的宝典!让我们翻开它吧! \ No newline at end of file diff --git a/src/ch19-01-unsafe-rust.md b/src/ch19-01-unsafe-rust.md index b4f61f8..a288c03 100644 --- a/src/ch19-01-unsafe-rust.md +++ b/src/ch19-01-unsafe-rust.md @@ -1,10 +1,10 @@ ## 不安全 Rust -> [ch19-01-unsafe-rust.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch19-01-unsafe-rust.md) +> [ch19-01-unsafe-rust.md](https://github.com/rust-lang/book/blob/master/src/ch19-01-unsafe-rust.md) >
-> commit c2b43bd978a9176ac9aba22595e33d2335b2d04b +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -目前为止讨论过的代码都有 Rust 在编译时会强制执行的内存安全保证。然而,Rust 还隐藏有第二种语言,它不会强制执行这类内存安全保证:不安全 Rust。它与常规 Rust 代码无异,但是会提供额外的超级力量。 +目前为止讨论过的代码都有 Rust 在编译时会强制执行的内存安全保证。然而,Rust 还隐藏有第二种语言,它不会强制执行这类内存安全保证:这被称为 **不安全 Rust**(*unsafe Rust*)。它与常规 Rust 代码无异,但是会提供额外的超级力量。 不安全 Rust 之所以存在,是因为静态分析本质上是保守的。当编译器尝试确定一段代码是否支持某个保证时,拒绝一些有效的程序比接受无效程序要好一些。这必然意味着有时代码可能是合法的,但是 Rust 不这么认为!在这种情况下,可以使用不安全代码告诉编译器,“相信我,我知道我在干什么。”这么做的缺点就是你只能靠自己了:如果不安全代码出错了,比如解引用空指针,可能会导致不安全的内存使用。 @@ -12,14 +12,14 @@ ### 不安全的超级力量 -可以通过 `unsafe` 关键字来切换到不安全 Rust,接着可以开启一个新的存放不安全代码的块。这里有四类可以在不安全 Rust 中进行而不能用于安全 Rust 的操作。称之为 “不安全的超级力量。”这些超级力量是: +可以通过 `unsafe` 关键字来切换到不安全 Rust,接着可以开启一个新的存放不安全代码的块。这里有四类可以在不安全 Rust 中进行而不能用于安全 Rust 的操作,它们称之为 “不安全的超级力量。” 这些超级力量是: -1. 解引用裸指针 -2. 调用不安全的函数或方法 -3. 访问或修改可变静态变量 -4. 实现不安全 trait +* 解引用裸指针 +* 调用不安全的函数或方法 +* 访问或修改可变静态变量 +* 实现不安全 trait -有一点很重要,`unsafe` 并不会关闭借用检查器或禁用任何其他 Rust 安全检查:如果在不安全代码中使用引用,其仍会被检查。`unsafe` 关键字只是提供了那四个不会被编译器检查内存安全的功能。你仍然能在不安全块中获得某种程度的安全! +有一点很重要,`unsafe` 并不会关闭借用检查器或禁用任何其他 Rust 安全检查:如果在不安全代码中使用引用,其仍会被检查。`unsafe` 关键字只是提供了那四个不会被编译器检查内存安全的功能。你仍然能在不安全块中获得某种程度的安全。 再者,`unsafe` 不意味着块中的代码就一定是危险的或者必然导致内存安全问题:其意图在于作为程序员你将会确保 `unsafe` 块中的代码以有效的方式访问内存。 @@ -31,24 +31,17 @@ ### 解引用裸指针 -回到第四章的 “悬垂引用” 部分,那里提到了编译器会确保引用总是有效的。不安全 Rust 有两个被称为 **裸指针**(*raw pointers*)的类似于引用的新类型。和引用一样,裸指针是可变或不可变的,分别写作 `*const T` 和 `*mut T`。这里的星号不是解引用运算符;它是类型名称的一部分。在裸指针的上下文中,“裸指针” 意味着指针解引用之后不能直接赋值。 +回到第四章的 “悬垂引用” 部分,那里提到了编译器会确保引用总是有效的。不安全 Rust 有两个被称为 **裸指针**(*raw pointers*)的类似于引用的新类型。和引用一样,裸指针是可变或不可变的,分别写作 `*const T` 和 `*mut T`。这里的星号不是解引用运算符;它是类型名称的一部分。在裸指针的上下文中,**不可变** 意味着指针解引用之后不能直接赋值。 与引用和智能指针的区别在于,记住裸指针 -- 允许忽略借用规则,可以同时拥有不可变和可变的指针,或多个指向相同位置的可变指针 -- 不保证指向有效的内存 -- 允许为空 -- 不能实现任何自动清理功能 +* 允许忽略借用规则,可以同时拥有不可变和可变的指针,或多个指向相同位置的可变指针 +* 不保证指向有效的内存 +* 允许为空 +* 不能实现任何自动清理功能 通过去掉 Rust 强加的保证,你可以放弃安全保证以换取性能或使用另一个语言或硬件接口的能力,此时 Rust 的保证并不适用。 - - - 示例 19-1 展示了如何从引用同时创建不可变和可变裸指针。 ```rust @@ -60,16 +53,11 @@ let r2 = &mut num as *mut i32; 示例 19-1: 通过引用创建裸指针 - - - -注意这里没有引入 `unsafe` 关键字 ———— 可以在安全代码中 **创建** 裸指针,只是不能在不安全块之外 **解引用** 裸指针,稍后便会看到。 +注意这里没有引入 `unsafe` 关键字。可以在安全代码中 **创建** 裸指针,只是不能在不安全块之外 **解引用** 裸指针,稍后便会看到。 这里使用 `as` 将不可变和可变引用强转为对应的裸指针类型。因为直接从保证安全的引用来创建他们,可以知道这些特定的裸指针是有效,但是不能对任何裸指针做出如此假设。 -接下来会创建一个不能确定其有效性的裸指针,示例 19-2 展示了如何创建一个指向任意内存地址的裸指针。尝试使用任意内存是未定义行为:此地址可能有数据也可能没有,编译器可能会优化掉这个内存访问,或者程序可能会出现段错误(segfault)。通常没有好的理由编写这样的代码,不过却是可行的: +接下来会创建一个不能确定其有效性的裸指针,示例 19-2 展示了如何创建一个指向任意内存地址的裸指针。尝试使用任意内存是未定义行为:此地址可能有数据也可能没有,编译器可能会优化掉这个内存访问,或者程序可能会出现段错误(segmentation fault)。通常没有好的理由编写这样的代码,不过却是可行的: ```rust let address = 0x012345usize; @@ -78,9 +66,9 @@ let r = address as *const i32; 示例 19-2: 创建指向任意内存地址的裸指针 -记得我们说过可以在安全代码中创建裸指针,不过不能 **解引用** 裸指针和读取其指向的数据。现在我们要做的就是对裸指针使用解引用运算符 `*`,只要求一个 `unsafe` 块,如示例 19-3 所示: +记得我们说过可以在安全代码中创建裸指针,不过不能 **解引用** 裸指针和读取其指向的数据。现在我们要做的就是对裸指针使用解引用运算符 `*`,这需要一个 `unsafe` 块,如示例 19-3 所示: -```rust +```rust,unsafe let mut num = 5; let r1 = &num as *const i32; @@ -98,19 +86,15 @@ unsafe { 还需注意示例 19-1 和 19-3 中创建了同时指向相同内存位置 `num` 的裸指针 `*const i32` 和 `*mut i32`。相反如果尝试创建 `num` 的不可变和可变引用,这将无法编译因为 Rust 的所有权规则不允许拥有可变引用的同时拥有不可变引用。通过裸指针,就能够同时创建同一地址的可变指针和不可变指针,若通过可变指针修改数据,则可能潜在造成数据竞争。请多加小心! -既然存在这么多的危险,为何还要使用裸指针呢?一个主要的应用场景便是调用 C 代码接口,这在下一部分不安全函数中会讲到。另一个场景是构建借用检查器无法理解的安全抽象。让我们先介绍不安全函数,接着看一看使用不安全代码的安全抽象的例子。 +既然存在这么多的危险,为何还要使用裸指针呢?一个主要的应用场景便是调用 C 代码接口,这在下一部分 “调用不安全函数或方法” 中会讲到。另一个场景是构建借用检查器无法理解的安全抽象。让我们先介绍不安全函数,接着看一看使用不安全代码的安全抽象的例子。 ### 调用不安全函数或方法 第二类要求使用不安全块的操作是调用不安全函数。不安全函数和方法与常规函数方法十分类似,除了其开头有一个额外的 `unsafe`。`unsafe` 表明我们作为程序需要满足其要求,因为 Rust 不会保证满足这些要求。通过在 `unsafe` 块中调用不安全函数,我们表明已经阅读过此函数的文档并对其是否满足函数自身的契约负责。 - - - 如下是一个没有做任何操作的不安全函数 `dangerous` 的例子: -```rust +```rust,unsafe unsafe fn dangerous() {} unsafe { @@ -128,7 +112,7 @@ error[E0133]: call to unsafe function requires unsafe function or block | ^^^^^^^^^^^ call to unsafe function ``` -通过将 `dangerous` 调用插入 `unsafe` 块中,我们就向 Rust 保证了我们已经阅读过函数的文档,理解如何正确,并验证过所有内容的正确性。 +通过将 `dangerous` 调用插入 `unsafe` 块中,我们就向 Rust 保证了我们已经阅读过函数的文档,理解如何正确使用,并验证过其满足函数的契约。 不安全函数体也是有效的 `unsafe` 块,所以在不安全函数中进行另一个不安全操作时无需新增额外的 `unsafe` 块。 @@ -149,11 +133,9 @@ assert_eq!(b, &mut [4, 5, 6]); 示例 19-4: 使用安全的 `split_at_mut` 函数 -这个函数无法只通过安全 Rust 实现。一个尝试可能看起来像示例 19-5,它不能编译。处于简单考虑,我们将 `split_at_mut` 实现为函数而不是方法,并只处理 `i32` 值而非泛型 `T` 的 slice。 - -用安全的Rust代码是不能实现这个函数的. 如果要试一下用安全的Rust来实现它可以参考例19-5. 简单起见, 我们把`split_at_mut`实现成一个函数而不是一个方法, 这个函数只处理`i32`类型的切片而不是泛型类型`T`的切片: +这个函数无法只通过安全 Rust 实现。一个尝试可能看起来像示例 19-5,它不能编译。出于简单考虑,我们将 `split_at_mut` 实现为函数而不是方法,并只处理 `i32` 值而非泛型 `T` 的 slice。 -```rust,ignore +```rust,ignore,does_not_compile fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { let len = slice.len(); @@ -166,11 +148,11 @@ fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { 示例 19-5: 尝试只使用安全 Rust 来实现 `split_at_mut` -此函数有限获取 slice 的长度,然后通过检查参数是否小于或等于这个长度来断言参数所给定的索引位于 slice 当中。该断言意味着如果传入的索引比要分割的 slice 的索引更大,此函数在尝试使用这个索引前 panic。 +此函数首先获取 slice 的长度,然后通过检查参数是否小于或等于这个长度来断言参数所给定的索引位于 slice 当中。该断言意味着如果传入的索引比要分割的 slice 的索引更大,此函数在尝试使用这个索引前 panic。 次后我们在一个元组中返回两个可变的 slice:一个从原始 slice 的开头直到 `mid` 索引,另一个从 `mid` 直到原 slice 的结尾。 -如果尝试编译此代码,会得到一个错误: +如果尝试编译示例 19-5 的代码,会得到一个错误: ```text error[E0499]: cannot borrow `*slice` as mutable more than once at a time @@ -184,11 +166,11 @@ error[E0499]: cannot borrow `*slice` as mutable more than once at a time | - first borrow ends here ``` -Rust 的借用检查器不能理解我们要借用这个 slice 的两个不同部分:它只知道我们借用了同一个 slice 两次。本质上借用 slice 的不同部分是可以的,因为这样两个 slice 不会重叠,不过 Rust 还没有智能到理解这些。当我们知道某些事是可以的而 Rust 不知道的时候,就是触及不安全代码的时候了 +Rust 的借用检查器不能理解我们要借用这个 slice 的两个不同部分:它只知道我们借用了同一个 slice 两次。本质上借用 slice 的不同部分是可以的,因为结果两个 slice 不会重叠,不过 Rust 还没有智能到能够理解这些。当我们知道某些事是可以的而 Rust 不知道的时候,就是触及不安全代码的时候了 示例 19-6 展示了如何使用 `unsafe` 块,裸指针和一些不安全函数调用来实现 `split_at_mut`: -```rust +```rust,unsafe use std::slice; fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { @@ -206,7 +188,7 @@ fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { 示例 19-6: 在 `split_at_mut` 函数的实现中使用不安全代码 -回忆第四章的 “Slice” 部分,slice 是一个指向一些数据的指针,并带有该 slice 的长度。可以使用 `len` 方法获取 slice 的长度,使用 `as_mut_ptr` 方法访问 slice 的裸指针。在这个例子中,因为有一个 `i32` 值的可变 slice,`as_mut_ptr` 返回一个 `*mut i32` 类型的裸指针,储存在 `ptr` 变量中。 +回忆第四章的 “Slice 类型” 部分,slice 是一个指向一些数据的指针,并带有该 slice 的长度。可以使用 `len` 方法获取 slice 的长度,使用 `as_mut_ptr` 方法访问 slice 的裸指针。在这个例子中,因为有一个 `i32` 值的可变 slice,`as_mut_ptr` 返回一个 `*mut i32` 类型的裸指针,储存在 `ptr` 变量中。 我们保持索引 `mid` 位于 slice 中的断言。接着是不安全代码:`slice::from_raw_parts_mut` 函数获取一个裸指针和一个长度来创建一个 slice。这里使用此函数从 `ptr` 中创建了一个有 `mid` 个项的 slice。之后在 `ptr` 上调用 `offset` 方法并使用 `mid` 作为参数来获取一个从 `mid` 开始的裸指针,使用这个裸指针并以 `mid` 之后项的数量为长度创建一个 slice。 @@ -216,33 +198,30 @@ fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { 与此相对,示例 19-7 中的 `slice::from_raw_parts_mut` 在使用 slice 时很有可能会崩溃。这段代码获取任意内存地址并创建了一个长为一万的 slice: -```rust +```rust,unsafe use std::slice; -let address = 0x012345usize; +let address = 0x01234usize; let r = address as *mut i32; -let slice = unsafe { +let slice : &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) }; ``` 示例 19-7: 通过任意内存地址创建 slice -我们并不拥有这个任意地址的内存,也不能保证这段代码创建的 slice 包含有效的 `i32` 值。试图使用臆测为有效的 `slice` 会导致未定义的行为。 +我们并不拥有这个任意地址的内存,也不能保证这段代码创建的 slice 包含有效的 `i32` 值。试图使用臆测为有效的 `slice` 会导致未定义的行为。如果我们没有注意将 `address` 向 4(字节)对齐(`i32` 的对齐方式),那么甚至调用 `slice::from_raw_parts_mut` 已经是为定义行为了 —— slice 必须总是对齐的,即使它没有被使用(哪怕甚至为空)。 #### 使用 `extern` 函数调用外部代码 有时你的 Rust 代码可能需要与其他语言编写的代码交互。为此 Rust 有一个关键字,`extern`,有助于创建和使用 **外部函数接口**(*Foreign Function Interface*, FFI)。外部函数接口是一个编程语言用以定义函数的方式,其允许不同(外部)编程语言调用这些函数。 - - - 示例 19-8 展示了如何集成 C 标准库中的 `abs` 函数。`extern` 块中声明的函数在 Rust 代码中总是不安全的。因为其他语言不会强制执行 Rust 的规则且 Rust 无法检查它们,所以确保其安全是程序员的责任: 文件名: src/main.rs -```rust +```rust,unsafe extern "C" { fn abs(input: i32) -> i32; } @@ -258,29 +237,20 @@ fn main() { 在 `extern "C"` 块中,列出了我们希望能够调用的另一个语言中的外部函数的签名和名称。`"C"` 部分定义了外部函数所使用的 **应用程序接口**(*application binary interface*,ABI) —— ABI 定义了如何在汇编语言层面调用此函数。`"C"` ABI 是最常见的,并遵循 C 编程语言的 ABI。 - - -##### 通过其它语言调用 Rust 函数 - -也可以使用 `extern` 来创建一个允许其他语言调用 Rust 函数的接口。不同于 `extern` 块,就在 `fn` 关键字之前增加 `extern` 关键字并指定所用到的 ABI。还需增加 `#[no_mangle]` 注解来告诉 Rust 编译器不要 mangle 此函数的名称。mangle 发生于当编译器将我们指定的函数名修改为不同的名称时,这会增加用于其他编译过程的额外信息,不过会使其名称更难以阅读。每一个编程语言的编译器都会以稍微不同的方式 mangle 函数名,所以为了使 Rust 函数能在其他语言中指定,必须禁用 Rust 编译器的 name mangling。 - - - - -在如下的例子中,一旦其编译为动态库并从 C 语言中链接,`call_from_c` 函数就能够在 C 代码中访问: - -```rust -#[no_mangle] -pub extern "C" fn call_from_c() { - println!("Just called a Rust function from C!"); -} -``` - -`extern` 的使用无需 `unsafe`。 - - +> #### 从其它语言调用 Rust 函数 +> +> 也可以使用 `extern` 来创建一个允许其他语言调用 Rust 函数的接口。不同于 `extern` 块,就在 `fn` 关键字之前增加 `extern` 关键字并指定所用到的 ABI。还需增加 `#[no_mangle]` 注解来告诉 Rust 编译器不要 mangle 此函数的名称。*Mangling* 发生于当编译器将我们指定的函数名修改为不同的名称时,这会增加用于其他编译过程的额外信息,不过会使其名称更难以阅读。每一个编程语言的编译器都会以稍微不同的方式 mangle 函数名,所以为了使 Rust 函数能在其他语言中指定,必须禁用 Rust 编译器的 name mangling。 +> +> 在如下的例子中,一旦其编译为动态库并从 C 语言中链接,`call_from_c` 函数就能够在 C 代码中访问: +> +> ```rust +> #[no_mangle] +> pub extern "C" fn call_from_c() { +> println!("Just called a Rust function from C!"); +> } +> ``` +> +> `extern` 的使用无需 `unsafe`。 ### 访问或修改可变静态变量 @@ -308,7 +278,7 @@ fn main() { 文件名: src/main.rs -```rust +```rust,unsafe static mut COUNTER: u32 = 0; fn add_to_count(inc: u32) { @@ -336,7 +306,7 @@ fn main() { 最后一个只能用在 `unsafe` 中的操作是实现不安全 trait。当至少有一个方法中包含编译器不能验证的不变量时 trait 是不安全的。可以在 `trait` 之前增加 `unsafe` 关键字将 trait 声明为 `unsafe`,同时 trait 的实现也必须标记为 `unsafe`,如示例 19-11 所示: -```rust +```rust,unsafe unsafe trait Foo { // methods go here } @@ -354,4 +324,4 @@ unsafe impl Foo for i32 { ### 何时使用不安全代码 -使用 `unsafe` 来进行这四个操作之一是没有问题的,甚至是不需要深思熟虑的,不过使得 `unsafe` 代码正确也实属不易因为编译器不能帮助保证内存安全。当有理由使用 `unsafe` 代码时,是可以这么做的,通过使用显式的 `unsafe` 标注使得在出现错误时易于追踪问题的源头。 +使用 `unsafe` 来进行这四个操作(超级力量)之一是没有问题的,甚至是不需要深思熟虑的,不过使得 `unsafe` 代码正确也实属不易因为编译器不能帮助保证内存安全。当有理由使用 `unsafe` 代码时,是可以这么做的,通过使用显式的 `unsafe` 标注使得在出现错误时易于追踪问题的源头。 diff --git a/src/ch19-02-advanced-lifetimes.md b/src/ch19-02-advanced-lifetimes.md index 1eb10da..24ee170 100644 --- a/src/ch19-02-advanced-lifetimes.md +++ b/src/ch19-02-advanced-lifetimes.md @@ -1,26 +1,23 @@ ## 高级生命周期 -> [ch19-02-advanced-lifetimes.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch19-02-advanced-lifetimes.md) +> [ch19-02-advanced-lifetimes.md](https://github.com/rust-lang/book/blob/master/src/ch19-02-advanced-lifetimes.md) >
-> commit f7f5e4835c1c4f8ddb502a1dd09a1584ed6f4b6f +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 回顾第十章 “生命周期与引用有效性” 部分,我们学习了怎样使用生命周期参数注解引用来帮助 Rust 理解不同引用的生命周期如何相互联系。我们理解了每一个引用都有生命周期,不过大部分情况 Rust 允许我们省略生命周期。这里我们会看到三个还未涉及到的生命周期高级特征: * 生命周期子类型(lifetime subtyping),一个确保某个生命周期长于另一个生命周期的方式 * 生命周期 bound(lifetime bounds),用于指定泛型引用的生命周期 * trait 对象生命周期(trait object lifetimes),以及他们是如何推断的,以及何时需要指定 - - - +* 匿名生命周期:使(生命周期)省略更为明显 ### 生命周期子类型确保某个生命周期长于另一个生命周期 -生命周期子类型是一个指定某个生命周期应该长于另一个生命周期的方式。为了探索生命周期子类型,想象一下我们想要编写一个解析器。为此会有一个储存了需要解析的字符串的引用的结构体 `Context`。解析器将会解析字符串并返回成功或失败。其实现看起来像示例 19-12 中的代码,除了缺少了必须的生命周期注解,所以这还不能编译: +生命周期子类型是一个指定某个生命周期应该长于另一个生命周期的方式。为了探索生命周期子类型,想象一下我们想要编写一个解析器。为此会有一个储存了需要解析的字符串的引用的结构体 `Context`。解析器将会解析字符串并返回成功或失败。解析器需要借用 `Context` 来进行解析。其实现看起来像示例 19-12 中的代码,除了缺少了必须的生命周期注解,所以这还不能编译: 文件名: src/lib.rs -```rust,ignore +```rust,ignore,does_not_compile struct Context(&str); struct Parser { @@ -38,23 +35,11 @@ impl Parser { 编译代码会导致一个表明 Rust 期望 `Context` 中字符串 slice 和 `Parser` 中 `Context` 的引用的生命周期的错误。 - - - 为了简单起见,`parse` 方法返回 `Result<(), &str>`。也就是说,成功时不做任何操作,失败时则返回字符串 slice 没有正确解析的部分。真实的实现将会包含比这更多的错误信息,并将会在解析成功时返回实际结果,不过我们将去掉这些部分的实现,因为他们与这个例子的生命周期部分并不相关。 为了保持代码简单,我们不准备实际编写任何解析逻辑。解析逻辑的某处非常有可能通过返回引用输入中无效部分的错误来处理无效输入,而考虑到生命周期,这个引用是使得这个例子有趣的地方。所以我们将假设解析器的逻辑为输入的第一个字节之后是无效的。注意如果第一个字节并不位于一个有效的字符范围内(比如 Unicode)代码将会 panic;这里又一次简化了例子以专注于涉及到的生命周期。 - - - -为了使代码能够编译,我们需要放入 `Context` 中字符串 slice 和 `Parser` 中 `Context` 引用的生命周期参数。最直接的方法是在每处都使用相同的生命周期,如示例 19-13 所示: - -那么我们如何为 `Context` 中的字符串 slice 和 `Parser` 中 `Context` 的引用放入生命周期参数呢?最直接的方法是在每处都使用相同的生命周期,如列表 19-13 所示: +为了使代码能够编译,我们需要放入 `Context` 中字符串 slice 和 `Parser` 中 `Context` 引用的生命周期参数。最直接的方法是在每处都使用相同的生命周期,如示例 19-13 所示。回忆第十章 “结构体定义中的生命周期注解” 部分,`struct Context<'a>`、`struct Parser<'a>` 和 `impl<'a>` 每一个都声明了一个新的生命周期参数。虽然这些名字碰巧都一样,例子中声明的三个生命周期参数并不相关。 文件名: src/lib.rs @@ -72,21 +57,15 @@ impl<'a> Parser<'a> { } ``` -示例 19-13: 将所有 `Context` 和 `Parser` 中的引用标注为相同的生命周期参数 +示例 19-13: 将所有 `Context` 和 `Parser` 中的引用标注生命周期参数 这次可以编译了,并告诉了 Rust `Parser` 存放了一个 `Context` 的引用,拥有生命周期 `'a`,且 `Context` 存放了一个字符串 slice,它也与 `Parser` 中 `Context` 的引用存在的一样久。Rust 编译器的错误信息表明这些引用需要生命周期参数,现在我们增加了这些生命周期参数。 - - - - - 接下来,在示例 19-14 中,让我们编写一个获取 `Context` 的实例,使用 `Parser` 来解析其内容,并返回 `parse` 的返回值的函数。这还不能运行: 文件名: src/lib.rs -```rust,ignore +```rust,ignore,does_not_compile fn parse_context(context: Context) -> Result<(), &str> { Parser { context: &context }.parse() } @@ -130,14 +109,9 @@ note: borrowed value must be valid for the anonymous lifetime #1 defined on the | |_^ ``` -这些错误表明我们创建的两个 `Parser` 实例和 `context` 参数从 `Parser` 被创建开始一直存活到 `parse_context` 函数结束,不过他们都需要在整个函数的生命周期中都有效。 - -换句话说,`Parser` 和 `context` 需要比整个函数 **长寿**(*outlive*)并在函数开始之前和结束之后都有效以确保代码中的所有引用始终是有效的。虽然我们创建的两个 `Parser` 和 `context` 参数在函数的结尾就离开了作用域(因为 `parse_context` 获取了 `context` 的所有权)。 +这些错误表明我们创建的两个 `Parser` 实例和 `context` 参数从 `Parser` 被创建开始一直存活到 `parse_context` 函数结束。不过他们都需要在整个函数的生命周期中都有效。 - - +换句话说,`Parser` 和 `context` 需要比整个函数 **长寿**(*outlive*)并在函数开始之前和结束之后都有效以确保代码中的所有引用始终是有效的。虽然我们创建的两个 `Parser` 和 `context` 参数在函数的结尾就离开了作用域,因为 `parse_context` 获取了 `context` 的所有权。 为了理解为什么会得到这些错误,让我们再次看看示例 19-13 中的定义,特别是 `parse` 方法的签名中的引用: @@ -145,9 +119,6 @@ previous paragraph. Is there something that's unclear? /Carol --> fn parse(&self) -> Result<(), &str> { ``` - - - 还记得(生命周期)省略规则吗?如果标注了引用生命周期而不加以省略,签名看起来应该是这样: ```rust,ignore @@ -168,7 +139,7 @@ Rust 认为我们尝试返回一个在函数结尾离开作用域的值,因为 文件名: src/lib.rs -```rust,ignore +```rust,ignore,does_not_compile struct Context<'s>(&'s str); struct Parser<'c, 's> { @@ -239,18 +210,15 @@ struct Parser<'c, 's: 'c> { 在第十章 “trait bound” 部分,我们讨论了如何在泛型类型上使用 trait bound。也可以像泛型那样为生命周期参数增加限制,这被称为 **生命周期 bound**(*lifetime bounds*)。生命周期 bound 帮助 Rust 验证泛型的引用不会存在的比其引用的数据更久。 - - - 例如,考虑一下一个封装了引用的类型。回忆一下第十五章 “`RefCell` 和内部可变性模式” 部分的 `RefCell` 类型:其 `borrow` 和 `borrow_mut` 方法分别返回 `Ref` 和 `RefMut` 类型。这些类型是引用的封装,他们在运行时记录检查借用规则。`Ref` 结构体的定义如示例 19-16 所示,目前还不带有生命周期 bound: 文件名: src/lib.rs -```rust,ignore +```rust,ignore,does_not_compile struct Ref<'a, T>(&'a T); ``` -示例 19-16: 定义结构体来封装泛型的引用;开始时没有生命周期约束 +示例 19-16: 定义结构体来封装泛型的引用,没有生命周期 bound 若不显式限制生命周期 `'a` 为与泛型参数 `T` 有关,会得到一个错误因为 Rust 不知道泛型 `T` 会存活多久: @@ -280,8 +248,6 @@ consider adding an explicit lifetime bound `T: 'a` so that the reference type 示例 19-17 展示了如何按照这个建议,在声明泛型 `T` 时指定生命周期 bound。。 -列表 19-17 展示了按照这个建议,在声明泛型 `T` 时指定生命周期约束。 - ```rust struct Ref<'a, T: 'a>(&'a T); ``` @@ -318,7 +284,7 @@ impl<'a> Red for Ball<'a> { } fn main() { let num = 5; - let obj = Box::new(Ball { diameter: &num }) as Box; + let obj = Box::new(Ball { diameter: &num }) as Box; } ``` @@ -333,4 +299,43 @@ fn main() { 当必须明确指定时,可以为像 `Box` 这样的 trait 对象增加生命周期 bound,根据需要使用语法 `Box` 或 `Box`。正如其他的 bound,这意味着任何 `Red` trait 的实现如果在内部包含有引用, 这些引用就必须拥有与 trait 对象 bound 中所指定的相同的生命周期。 +### 匿名生命周期 + +比方说有一个封装了一个字符串 slice 的结构体,如下: + +```rust +struct StrWrap<'a>(&'a str); +``` + +可以像这样编写一个返回它们的函数: + +```rust +# struct StrWrap<'a>(&'a str); +fn foo<'a>(string: &'a str) -> StrWrap<'a> { + StrWrap(string) +} +``` + +不过这里有很多的 `'a`!为了消除这些噪音,可以使用匿名生命周期,`'_`,如下: + +```rust +# struct StrWrap<'a>(&'a str); +fn foo(string: &str) -> StrWrap<'_> { + StrWrap(string) +} +``` + +`'_` 表明 “在此处使用省略的生命周期。” 这意味着我们仍然知道 `StrWrap` 包含一个引用,不过无需所有的生命周期注解来知道。 + +其也能用于 `impl`;例如: + +```rust,ignore +// 冗余 +impl<'a> fmt::Debug for StrWrap<'a> { + +// 省略 +impl fmt::Debug for StrWrap<'_> { + +``` + 接下来,让我们看看一些其他处理 trait 的高级功能吧! diff --git a/src/ch19-03-advanced-traits.md b/src/ch19-03-advanced-traits.md index 8307a36..6221151 100644 --- a/src/ch19-03-advanced-traits.md +++ b/src/ch19-03-advanced-traits.md @@ -1,8 +1,8 @@ ## 高级 trait -> [ch19-03-advanced-traits.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch19-03-advanced-traits.md) +> [ch19-03-advanced-traits.md](https://github.com/rust-lang/book/blob/master/src/ch19-03-advanced-traits.md) >
-> commit 9d5b9a573daf5fa0c98b3a3005badcea4a0a5211 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 第十章 “trait:定义共享的行为” 部分,我们第一次涉及到了 trait,不过就像生命周期一样,我们并没有覆盖一些较为高级的细节。现在我们更加了解 Rust 了,可以深入理解其本质了。 @@ -10,10 +10,6 @@ **关联类型**(*associated types*)是一个将类型占位符与 trait 相关联的方式,这样 trait 的方法签名中就可以使用这些占位符类型。trait 的实现者会针对特定的实现在这个类型的位置指定相应的具体类型。如此可以定义一个使用多种类型的 trait,直到实现此 trait 时都无需知道这些类型具体是什么。 - - - 本章所描述的大部分内容都非常少见。关联类型则比较适中;它们比本书其他的内容要少见,不过比本章中的很多内容要更常见。 一个带有关联类型的 trait 的例子是标准库提供的 `Iterator` trait。它有一个叫做 `Item` 的关联类型来替代遍历的值的类型。第十三章的 “`Iterator` trait 和 `next` 方法” 部分曾提到过 `Iterator` trait 的定义如示例 19-20 所示: @@ -21,17 +17,16 @@ to specify a type prior to use, is that right? --> ```rust pub trait Iterator { type Item; + fn next(&mut self) -> Option; } ``` 示例 19-20: `Iterator` trait 的定义中带有关联类型 `Item` -`Iterator` trait 有一个关联类型 `Item`。`Item` 是一个占位类型,同时 `next` 方法会返回 `Option` 类型的值。这个 trait 的实现者会指定 `Item` 的具体类型,然而不管实现者指定何种类型, `next` 方法都会返回一个包含了此具体类型值的 `Option`。 - -#### 关联类型 vs 泛型 +`Item` 是一个占位类型,同时 `next` 方法定义表明它返回 `Option` 类型的值。这个 trait 的实现者会指定 `Item` 的具体类型,然而不管实现者指定何种类型, `next` 方法都会返回一个包含了此具体类型值的 `Option`。 -这可能看起来像一个类似泛型的概念,因为它允许定义一个函数而不指定其可以处理的类型。那么为什么要使用关联类型呢? +关联类型看起来像一个类似泛型的概念,因为它允许定义一个函数而不指定其可以处理的类型。那么为什么要使用关联类型呢? 让我们通过一个在第十三章中出现的 `Counter` 结构体上实现 `Iterator` trait 的例子来检视其中的区别。在示例 13-21 中,指定了 `Item` 的类型为 `u32`: @@ -53,30 +48,26 @@ pub trait Iterator { } ``` -示例 19-21: 一个使用泛型的 `Iterator` trait 假象定义 +示例 19-21: 一个使用泛型的 `Iterator` trait 假想定义 区别在于当如示例 19-21 那样使用泛型时,则不得不在每一个实现中标注类型。这是因为我们也可以实现为 `Iterator for Counter`,或任何其他类型,这样就可以有多个 `Counter` 的 `Iterator` 的实现。换句话说,当 trait 有泛型参数时,可以多次实现这个 trait,每次需改变泛型参数的具体类型。接着当使用 `Counter` 的 `next` 方法时,必须提供类型注解来表明希望使用 `Iterator` 的哪一个实现。 -通过关联类型,则无需标注类型因为不能多次实现这个 trait。对于示例 19-20,我们只能选择一次 `Item` 会是什么类型,因为只能有一个 `impl Iterator for Counter`。当调用 `Counter` 的 `next` 时不必每次指定我们需要 `u32` 值的迭代器。 +通过关联类型,则无需标注类型因为不能多次实现这个 trait。对于示例 19-20 使用关联类型的定义,我们只能选择一次 `Item` 会是什么类型,因为只能有一个 `impl Iterator for Counter`。当调用 `Counter` 的 `next` 时不必每次指定我们需要 `u32` 值的迭代器。 ### 默认泛型类型参数和运算符重载 当使用泛型类型参数时,可以为泛型指定一个默认的具体类型。如果默认类型就足够的话,这消除了为具体类型实现 trait 的需要。为泛型类型指定默认类型的语法是在声明泛型类型时使用 ``。 -这种情况的一个非常好的例子是用于运算符重载。运算符重载是指在特定情况下自定义运算符(比如 `+`)行为的操作。 +这种情况的一个非常好的例子是用于运算符重载。**运算符重载**(*Operator overloading*)是指在特定情况下自定义运算符(比如 `+`)行为的操作。 - - - -Rust 并不允许创建自定义运算符或重载任意运算符,不过 `std::ops` 中所列出的运算符和相应的 trait **可以** 通过实现运算符相关 trait 来重载。例如,示例 19-22 中展示了如何在 `Point` 结构体上实现 `Add` trait 来重载 `+` 运算符,这样就可以将两个 `Point` 实例相加了: +Rust 并不允许创建自定义运算符或重载任意运算符,不过 `std::ops` 中所列出的运算符和相应的 trait 可以通过实现运算符相关 trait 来重载。例如,示例 19-22 中展示了如何在 `Point` 结构体上实现 `Add` trait 来重载 `+` 运算符,这样就可以将两个 `Point` 实例相加了: 文件名: src/main.rs ```rust use std::ops::Add; -#[derive(Debug,PartialEq)] +#[derive(Debug, PartialEq)] struct Point { x: i32, y: i32, @@ -113,14 +104,11 @@ trait Add { } ``` -这看来应该很熟悉,这是一个带有一个方法和一个关联类型的 trait。比较陌生的部分是尖括号中的 `RHS=Self`:这个语法叫做 **默认类型参数**(*default type parameters*)。`RHS` 是一个泛型类型参数 —— “right hand side” 的缩写 —— 它用于 `add` 方法中的 `rhs` 参数。如果实现 `Add` trait 时不指定 `RHS` 的具体类型,`RHS` 的类型将是默认的 `Self` 类型,也就是在其上实现 `Add` 的类型。 - - - +这看来应该很熟悉,这是一个带有一个方法和一个关联类型的 trait。比较陌生的部分是尖括号中的 `RHS=Self`:这个语法叫做 **默认类型参数**(*default type parameters*)。`RHS` 是一个泛型类型参数(“right hand side” 的缩写),它用于定义 `add` 方法中的 `rhs` 参数。如果实现 `Add` trait 时不指定 `RHS` 的具体类型,`RHS` 的类型将是默认的 `Self` 类型,也就是在其上实现 `Add` 的类型。 当为 `Point` 实现 `Add` 时,使用了默认的 `RHS`,因为我们希望将两个 `Point` 实例相加。让我们看看一个实现 `Add` trait 时希望自定义 `RHS` 类型而不是使用默认类型的例子 -这里有两个存放不同单元值的结构体,`Millimeters` 和 `Meters`。我们希望能够将毫米值与米值相加,并让 `Add` 的实现正确处理转换。可以为 `Millimeters` 实现 `Add` 并以 `Meters` 作为右手边,如示例 19-23 所示: +这里有两个存放不同单元值的结构体,`Millimeters` 和 `Meters`。我们希望能够将毫米值与米值相加,并让 `Add` 的实现正确处理转换。可以为 `Millimeters` 实现 `Add` 并以 `Meters` 作为 `RHS`,如示例 19-23 所示。 文件名: src/lib.rs @@ -145,12 +133,8 @@ impl Add for Millimeters { 默认参数类型主要用于如下两个方面: -1. 扩展类型而不破坏现有代码。 -2. 在大部分用户都不需要的特定情况进行自定义。 - - - +* 扩展类型而不破坏现有代码。 +* 在大部分用户都不需要的特定情况进行自定义。 标准库的 `Add` trait 就是一个第二个目的例子:大部分时候你会将两个相似的类型相加,不过它提供了自定义额外行为的能力。在 `Add` trait 定义中使用默认类型参数意味着大部分时候无需指定额外的参数。换句话说,一小部分实现的样板代码是不必要的,这样使用 trait 就更容易了。 @@ -160,14 +144,8 @@ it could be clearer /Carol--> Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法,也不能阻止为同一类型同时实现这两个 trait。甚至直接在类型上实现开始已经有的同名方法也是可能的! - - - 不过,当调用这些同名方法时,需要告诉 Rust 我们希望使用哪一个。考虑一下示例 19-24 中的代码,这里定义了 trait `Pilot` 和 `Wizard` 都拥有方法 `fly`。接着在一个本身已经实现了名为 `fly` 方法的类型 `Human` 上实现这两个 trait。每一个 `fly` 方法都进行了不同的操作: -甚至也可以直接在类型上实现相同名称的方法!那么为了能使用相同的名称调用每一个方法,需要告诉 Rust 我们希望使用哪个方法。考虑一下列表 19-27 中的代码,trait `Foo` 和 `Bar` 都拥有方法 `f`,并在结构体 `Baz` 上实现了这两个 trait,结构体也有一个叫做 `f` 的方法: - 文件名: src/main.rs ```rust @@ -342,7 +320,7 @@ A baby dog is called a Spot 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile fn main() { println!("A baby dog is called a {}", Animal::baby_name()); } @@ -410,7 +388,7 @@ A baby dog is called a puppy 有时我们可能会需要某个 trait 使用另一个 trait 的功能。在这种情况下,需要能够依赖相关的 trait 也被实现。这个所需的 trait 是我们实现的 trait 的 **父(超) trait**(*supertrait*)。 -例如我们希望创建一个带有 `outline_print` 方法的 trait `OutlinePrint`,它会打印出带有星号框的值。也就是说,如果 `Point` 实现了 `Display` 并返回 `(x, y)`,调用以 1 作为 `x` 和 3 作为 `y` 的 `Point` 实例的 `outline_print` 会显示如下: +例如我们希望创建一个带有 `outline_print` 方法的 trait `OutlinePrint`,它会打印出带有星号框的值。也就是说,如果 `Point` 实现了 `Display` 并返回 `(x, y)`,调用以 `1` 作为 `x` 和 `3` 作为 `y` 的 `Point` 实例的 `outline_print` 会显示如下: ```text ********** @@ -466,7 +444,7 @@ error[E0277]: the trait bound `Point: std::fmt::Display` is not satisfied | 20 | impl OutlinePrint for Point {} | ^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter; - try using `:?` instead if you are using a format string +try using `:?` instead if you are using a format string | = help: the trait `std::fmt::Display` is not implemented for `Point` ``` @@ -490,15 +468,13 @@ impl fmt::Display for Point { } ``` -那么在 `Point` 上实现 `OutlinePrint` trait 将能成功编译并可以在 `Point` 实例上调用 `outline_print` 来显示位于星号框中的点的值。 +那么在 `Point` 上实现 `OutlinePrint` trait 将能成功编译,并可以在 `Point` 实例上调用 `outline_print` 来显示位于星号框中的点的值。 ### newtype 模式用以在外部类型上实现外部 trait 在第十章的 “为类型实现 trait” 部分,我们提到了孤儿规则(orphan rule),它说明只要 trait 或类型对于当前 crate 是本地的话就可以在此类型上实现该 trait。一个绕开这个限制的方法是使用**newtype 模式**(*newtype pattern*),它涉及到在一个元组结构体(第五章 “用没有命名字段的元组结构体来创建不同的类型” 部分介绍了元组结构体)中创建一个新类型。这个元组结构体带有一个字段作为希望实现 trait 的类型的简单封装。接着这个封装类型对于 crate 是本地的,这样就可以在这个封装上实现 trait。“Newtype” 是一个源自(U.C.0079,逃)Haskell 编程语言的概念。使用这个模式没有运行时性能惩罚,这个封装类型在编译时就被省略了。 -例如,如果想要在 `Vec` 上实现 `Display`,而孤儿规则阻止我们直接这么做,因为 `Display` trait 和 `Vec` 都定义于我们的 crate 之外。可以创建一个包含 `Vec` 实例的 `Wrapper` 结构体,接着可以如列表 19-31 那样在 `Wrapper` 上实现 `Display` 并使用 `Vec` 的值: - -可以创建一个包含 `Vec` 实例的 `Wrapper` 结构体。接着可以如列表 19-30 那样在 `Wrapper` 上实现 `Display` 并使用 `Vec` 的值: +例如,如果想要在 `Vec` 上实现 `Display`,而孤儿规则阻止我们直接这么做,因为 `Display` trait 和 `Vec` 都定义于我们的 crate 之外。可以创建一个包含 `Vec` 实例的 `Wrapper` 结构体,接着可以如列表 19-31 那样在 `Wrapper` 上实现 `Display` 并使用 `Vec` 的值: 文件名: src/main.rs @@ -521,14 +497,8 @@ fn main() { 示例 19-31: 创建 `Wrapper` 类型封装 `Vec` 以便能够实现 `Display` -`Display` 的实现使用 `self.0` 来访问其内部的 `Vec`,因为 `Wrapper` 是元组结构体而 `Vec` 是结构体总位于索引 0 的项。接着就可以使用 `Wrapper` 中 `Display` 的功能了。 - - - +`Display` 的实现使用 `self.0` 来访问其内部的 `Vec`,因为 `Wrapper` 是元组结构体而 `Vec` 是结构体总位于索引 0 的项。接着就可以使用 `Wrapper` 中 `Display` 的功能了。 -此方法的缺点是,因为 `Wrapper` 是一个新类型,它没有定义于其值之上的方法;必须直接在 `Wrapper` 上实现 `Vec` 的所有方法,这样就可以代理到`self.0` 上 —— 这就允许我们完全像 `Vec` 那样对待 `Wrapper`。如果希望新类型拥有其内部类型的每一个方法,为封装类型实现 `Deref` trait(第十五章 “通过 `Deref` trait 将智能指针当作常规引用处理” 部分讨论过)并返回其内部类型是一种解决方案。如果不希望封装类型拥有所有内部类型的方法,比如为了限制封装类型的行为,则必须只自行实现所需的方法。 +此方法的缺点是,因为 `Wrapper` 是一个新类型,它没有定义于其值之上的方法;必须直接在 `Wrapper` 上实现 `Vec` 的所有方法,这样就可以代理到`self.0` 上 —— 这就允许我们完全像 `Vec` 那样对待 `Wrapper`。如果希望新类型拥有其内部类型的每一个方法,为封装类型实现 `Deref` trait(第十五章 “通过 `Deref` trait 将智能指针当作常规引用处理” 部分讨论过)并返回其内部类型是一种解决方案。如果不希望封装类型拥有所有内部类型的方法 —— 比如为了限制封装类型的行为 —— 则必须只自行实现所需的方法。 上面便是 newtype 模式如何与 trait 结合使用的;还有一个不涉及 trait 的实用模式。现在让我们将话题的焦点转移到一些与 Rust 类型系统交互的高级方法上来吧。