mirror of
https://github.com/KaiserY/trpl-zh-cn
synced 2024-11-09 00:31:27 +08:00
check to ch15-05
This commit is contained in:
parent
d50fb8b058
commit
5d99450d89
@ -2,15 +2,38 @@
|
||||
|
||||
> [ch15-03-drop.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-03-drop.md)
|
||||
> <br>
|
||||
> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
|
||||
> commit 721553e3a7b5ee9430cb548c8699b67be197b3f6
|
||||
|
||||
对于智能指针模式来说另一个重要的 trait 是`Drop`。`Drop`运行我们在值要离开作用域时执行一些代码。智能指针在被丢弃时会执行一些重要的清理工作,比如释放内存或减少引用计数。更一般的来讲,数据类型可以管理多于内存的资源,比如文件或网络连接,而使用`Drop`在代码处理完他们之后释放这些资源。我们在智能指针上下文中讨论`Drop`是因为其功能几乎总是用于实现智能指针。
|
||||
对于智能指针模式来说另一个重要的 trait 是 `Drop`。`Drop` 允许我们在值要离开作用域时执行一些代码。可以为任何类型提供 `Drop` trait 的实现,同时所指定的代码被用于释放类似于文件或网络连接的资源。我们在智能指针上下文中讨论 `Drop` 是因为其功能几乎总是用于实现智能指针。例如,`Box<T>` 自定义了 `Drop` 用来释放 box 所指向的堆空间。
|
||||
|
||||
在其他一些语言中,我们不得不记住在每次使用完智能指针实例后调用清理内存或资源的代码。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。在 Rust 中,可以指定一些代码应该在值离开作用域时被执行,而编译器会自动插入这些代码。这意味着无需记住在所有处理完这些类型实例后调用清理代码,而仍然不会泄露资源!
|
||||
在其他一些语言中,我们不得不记住在每次使用完智能指针实例后调用清理内存或资源的代码。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。在 Rust 中,可以指定一些代码应该在值离开作用域时被执行,而编译器会自动插入这些代码。
|
||||
|
||||
指定在值离开作用域时应该执行的代码的方式是实现`Drop` trait。`Drop` trait 要求我们实现一个叫做`drop`的方法,它获取一个`self`的可变引用。
|
||||
<!-- Are we saying that any code can be run, and that we can use that to clean
|
||||
up, or that this code that can be run is specifically always for clean up? -->
|
||||
<!-- I don't understand what the difference between those two choices are?
|
||||
/Carol -->
|
||||
|
||||
这意味着无需记住在所有处理完这些类型实例后调用清理代码,而仍然不会泄露资源!
|
||||
|
||||
指定在值离开作用域时应该执行的代码的方式是实现 `Drop` trait。`Drop` trait 要求实现一个叫做 `drop` 的方法,它获取一个 `self` 的可变引用。为了能够看出 Rust 何时调用 `drop`,让我们暂时使用 `println!` 语句实现 `drop`。
|
||||
|
||||
<!-- Why are we showing this as an example and not an example of it being used
|
||||
for clean up? -->
|
||||
<!-- To demonstrate the mechanics of implementing the trait and showing when
|
||||
this code gets run. It's hard to experience the cleaning up unless we print
|
||||
something. /Carol -->
|
||||
|
||||
示例 15-16 展示了唯一定制功能就是当其实例离开作用域时打印出 `Dropping CustomSmartPointer!` 的结构体 `CustomSmartPointer`。这会演示 Rust 何时运行 `drop` 函数:
|
||||
|
||||
<!-- Is this below just telling us how to adapt it for cleaning up instead?
|
||||
Maybe save it for when we have context for it? Instead of a `println!`
|
||||
statement, you'd fill in `drop` with whatever cleanup code your smart pointer
|
||||
needs to run: -->
|
||||
<!-- This is demonstrating what we need to do to use `Drop`, without getting
|
||||
into the complexities of what "cleaning up" might mean yet, just to give the
|
||||
reader an idea of when this code gets called and that it gets called
|
||||
automatically. We're building up to cleaning up. /Carol -->
|
||||
|
||||
示例 15-8 展示了并没有实际功能的结构体`CustomSmartPointer`,不过我们会在创建实例之后打印出`CustomSmartPointer created.`,而在实例离开作用域时打印出`Dropping CustomSmartPointer!`,这样就能看出每一段代码是何时被执行的。实际的项目中,我们应该在`drop`中清理任何智能指针运行所需要的资源,而不是这个例子中的`println!`语句:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -21,32 +44,57 @@ struct CustomSmartPointer {
|
||||
|
||||
impl Drop for CustomSmartPointer {
|
||||
fn drop(&mut self) {
|
||||
println!("Dropping CustomSmartPointer!");
|
||||
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let c = CustomSmartPointer { data: String::from("some data") };
|
||||
println!("CustomSmartPointer created.");
|
||||
println!("Wait for it...");
|
||||
let c = CustomSmartPointer { data: String::from("my stuff") };
|
||||
let d = CustomSmartPointer { data: String::from("other stuff") };
|
||||
println!("CustomSmartPointers created.");
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 15-8: 结构体 `CustomSmartPointer` 实现了 `Drop` trait, 我们能够放入代码以便在 `CustomSmartPointer` 离开作用域后进行清理</span>
|
||||
<span class="caption">示例 15-16:结构体 `CustomSmartPointer`,其实现了放置清理代码的 `Drop` trait</span>
|
||||
|
||||
`Drop` trait 位于 prelude 中,所以无需导入它。`drop`方法的实现调用了`println!`;这里是你需要放入实际关闭套接字代码的地方。在`main`函数中,我们创建一个`CustomSmartPointer`的新实例并打印出`CustomSmartPointer created.`以便在运行时知道代码运行到此处。在`main`的结尾,`CustomSmartPointer`的实例会离开作用域。注意我们没有显式调用`drop`方法:
|
||||
`Drop` trait 包含在 prelude 中,所以无需导入它。我们在 `CustomSmartPointer` 上实现了 `Drop` trait,并提供了一个调用 `println!` 的 `drop` 方法实现。`drop` 函数体是放置任何当类型实例离开作用域时期望运行的逻辑的地方。这里选择打印一些文本以展示 Rust 合适调用 `drop`。
|
||||
|
||||
当运行这个程序,我们会看到:
|
||||
<!-- Where you'd put this code, or where this code would be called? It seems
|
||||
laborious to write this clean up code wherever there's a print call? -->
|
||||
<!-- I'm not sure how you concluded that from what we had here, could you
|
||||
elaborate? /Carol -->
|
||||
|
||||
```
|
||||
CustomSmartPointer created.
|
||||
Wait for it...
|
||||
Dropping CustomSmartPointer!
|
||||
在 `main` 中,新建了一个 `CustomSmartPointer` 实例并打印出了 `CustomSmartPointer created.`。在 `main` 的结尾,`CustomSmartPointer` 的实例会离开作用域,而 Rust 会调用放置于 `drop` 方法中的代码,打印出最后的信息。注意无需显示调用 `drop` 方法:
|
||||
|
||||
当运行这个程序,会出现如下输出:
|
||||
|
||||
```text
|
||||
CustomSmartPointers created.
|
||||
Dropping CustomSmartPointer with data `other stuff`!
|
||||
Dropping CustomSmartPointer with data `my stuff`!
|
||||
```
|
||||
|
||||
被打印到屏幕上,它展示了 Rust 在实例离开作用域时自动调用了`drop`。
|
||||
当实例离开作用域 Rust 会自动调用 `drop`,并调用我们指定的代码。变量以被创创建时相反的顺序被丢弃,所以 `d` 在 `c` 之前被丢弃。这刚好给了我们一个 drop 方法如何工作的可视化指导,不过通常需要指定类型所需执行的清理代码而不是打印信息。
|
||||
|
||||
可以使用`std::mem::drop`函数来在值离开作用域之前丢弃它。这通常是不必要的;整个`Drop` trait 的要点在于它自动的帮我们处理清理工作。在第十六章讲到并发时我们会看到一个需要在离开作用域之前丢弃值的例子。现在知道这是可能的即可,`std::mem::drop`位于 prelude 中所以可以如示例 15-9 所示直接调用`drop`:
|
||||
<!-- Can you wrap this example up by saying what you would actually put in a
|
||||
drop method and why?-->
|
||||
<!-- Done /Carol -->
|
||||
|
||||
#### 通过 `std::mem::drop` 提早丢弃值
|
||||
|
||||
<!-- is this a new method from Drop or the same method? -->
|
||||
<!-- This is a new function. /Carol -->
|
||||
|
||||
Rust 当值离开作用域时自动插入 `drop` 调用,不能直接禁用这个功能。
|
||||
|
||||
|
||||
|
||||
被打印到屏幕上,它展示了 Rust 在实例离开作用域时自动调用了`drop`。通常也不需要禁用 `drop`;整个 `Drop` trait 存在的意义在于其是自动处理的。有时可能需要提早清理某个值。一个例子是当使用智能指针管理锁时;你可能希望强制运行 `drop` 方法来释放锁以便作用域中的其他代码可以获取锁。首先。让我们看看自己调用 `Drop` trait 的 `drop` 方法会发生什么,如示例 15-17 修改示例 15-16 中的 `main` 函数:
|
||||
|
||||
<!-- Above: I'm not following why we are doing this, if it's not necessary and
|
||||
we aren't going to cover it now anyway -- can you lay out why we're discussing
|
||||
this here? -->
|
||||
<!-- Done. /Carol -->
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -54,33 +102,80 @@ Dropping CustomSmartPointer!
|
||||
fn main() {
|
||||
let c = CustomSmartPointer { data: String::from("some data") };
|
||||
println!("CustomSmartPointer created.");
|
||||
drop(c);
|
||||
println!("Wait for it...");
|
||||
c.drop();
|
||||
println!("CustomSmartPointer dropped before the end of main.");
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 15-9: 在一个值离开作用域之前,调用 `std::mem::drop` 显式进行回收</span>
|
||||
<span class="caption">示例 15-17:尝试手动调用 `Drop` trait 的 `drop` 方法提早清理</span>
|
||||
|
||||
运行这段代码会打印出如下内容,因为`Dropping CustomSmartPointer!`在`CustomSmartPointer created.`和`Wait for it...`之间被打印出来,表明析构代码被执行了:
|
||||
如果尝试编译代码会得到如下错误:
|
||||
|
||||
```
|
||||
CustomSmartPointer created.
|
||||
Dropping CustomSmartPointer!
|
||||
Wait for it...
|
||||
```text
|
||||
error[E0040]: explicit use of destructor method
|
||||
--> src/main.rs:15:7
|
||||
|
|
||||
15 | c.drop();
|
||||
| ^^^^ explicit destructor calls not allowed
|
||||
```
|
||||
|
||||
注意不允许直接调用我们定义的`drop`方法:如果将示例 15-9 中的`drop(c)`替换为`c.drop()`,会得到一个编译错误表明`explicit destructor calls not allowed`。不允许直接调用`Drop::drop`的原因是 Rust 在值离开作用域时会自动插入`Drop::drop`,这样就会丢弃值两次。丢弃一个值两次可能会造成错误或破坏内存,所以 Rust 就不允许这么做。相应的可以调用`std::mem::drop`,它的定义是:
|
||||
错误信息表明不允许显式调用 `drop`。错误信息使用了术语 **析构函数**(*destructor*),这是一个清理实例的函数的通用编程概念。**析构函数** 对应创建实例的 **构造函数**。Rust 中的 `drop` 函数就是这么一个析构函数。
|
||||
|
||||
Rust 不允许我们显式调用 `drop` 因为 Rust 仍然会在 `main` 的结尾对值自动调用 `drop`,这会导致一个 **double free** 错误,因为 Rust 会尝试清理相同的值两次。
|
||||
|
||||
因为不能禁用当值离开作用域时自动插入的 `drop`,并且不能显示调用 `drop`,如果我们需要提早清理值,可以使用 `std::mem::drop` 函数。
|
||||
|
||||
`std::mem::drop` 函数不同于 `Drop` trait 中的 `drop` 方法。可以通过传递希望提早强制丢弃的值作为参数。`std::mem::drop` 位于 prelude,所以我们可以修改示例 15-16 中的 `main` 来调用 `drop` 函数如示例 15-18 所示:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust
|
||||
pub mod std {
|
||||
pub mod mem {
|
||||
pub fn drop<T>(x: T) { }
|
||||
}
|
||||
# struct CustomSmartPointer {
|
||||
# data: String,
|
||||
# }
|
||||
#
|
||||
# impl Drop for CustomSmartPointer {
|
||||
# fn drop(&mut self) {
|
||||
# println!("Dropping CustomSmartPointer!");
|
||||
# }
|
||||
# }
|
||||
#
|
||||
fn main() {
|
||||
let c = CustomSmartPointer { data: String::from("some data") };
|
||||
println!("CustomSmartPointer created.");
|
||||
drop(c);
|
||||
println!("CustomSmartPointer dropped before the end of main.");
|
||||
}
|
||||
```
|
||||
|
||||
这个函数对于`T`是泛型的,所以可以传递任何值。这个函数的函数体并没有任何实际内容,所以它也不会利用其参数。这个空函数的作用在于`drop`获取其参数的所有权,它意味着在这个函数结尾`x`离开作用域时`x`会被丢弃。
|
||||
<span class="caption">示例 15-18: 在值离开作用域之前调用 `std::mem::drop` 显式清理</span>
|
||||
|
||||
运行这段代码会打印出如下:
|
||||
|
||||
```text
|
||||
CustomSmartPointer created.
|
||||
Dropping CustomSmartPointer with data `some data`!
|
||||
CustomSmartPointer dropped before the end of main.
|
||||
```
|
||||
|
||||
<!-- What's the destructor code, here? We haven't mentioned that before, not in
|
||||
this chapter in any case -->
|
||||
<!-- I added a definition for destructor a few paragraphs above, the first time
|
||||
we see it in an error message. /Carol -->
|
||||
|
||||
```Dropping CustomSmartPointer with data `some data`!``` 出现在 `CustomSmartPointer created.` 和 `CustomSmartPointer dropped before the end of main.` 之间,表明了 `drop` 方法被调用了并在此丢弃了 `c`。
|
||||
|
||||
<!-- How does this show that the destructor code (is that drop?) is called? Is
|
||||
this correct, above?-->
|
||||
<!-- The order of what gets printed shows that the drop code is called.
|
||||
/Carol-->
|
||||
|
||||
`Drop` trait 实现中指定的代码可以用于许多方面来使得清理变得方便和安全:比如可以用其创建我们自己的内存分配器!通过 `Drop` trait 和 Rust 所有权系统,你无需担心之后清理代码,Rust 会自动考虑这些问题。
|
||||
|
||||
我们也无需担心意外的清理掉仍在使用的值,这会造成编译器错误:所有权系统确保引用总是有效的,也会确保 `drop` 只会在值不再被使用时被调用一次。
|
||||
|
||||
|
||||
|
||||
使用`Drop` trait 实现指定的代码在很多方面都使得清理值变得方便和安全:比如可以使用它来创建我们自己的内存分配器!通过`Drop` trait 和 Rust 所有权系统,就无需担心之后清理代码,因为 Rust 会自动考虑这些问题。如果代码在值仍被使用时就清理它会出现编译错误,因为所有权系统确保了引用总是有效的,这也就保证了`drop`只会在值不再被使用时被调用一次。
|
||||
|
||||
现在我们学习了`Box<T>`和一些智能指针的特性,让我们聊聊一些其他标准库中定义的拥有各种实用功能的智能指针。
|
||||
现在我们学习了 `Box<T>` 和一些智能指针的特性,让我们聊聊一些其他标准库中定义的智能指针。
|
@ -2,27 +2,35 @@
|
||||
|
||||
> [ch15-04-rc.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-04-rc.md)
|
||||
> <br>
|
||||
> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
|
||||
> commit 071b97540bca12fd416d2ea7a2daa5d3e9c74400
|
||||
|
||||
大部分情况下所有权是非常明确的:可以准确的知道哪个变量拥有某个值。然而并不总是如此;有时确实可能需要多个所有者。为此,Rust 有一个叫做`Rc<T>`的类型。它的名字是**引用计数**(*reference counting*)的缩写。引用计数意味着它记录一个值引用的数量来知晓这个值是否仍在被使用。如果这个值有零个引用,就知道可以在没有有效引用的前提下清理这个值。
|
||||
大部分情况下所有权是非常明确的:可以准确的知道哪个变量拥有某个值。然而,有些情况单个值可能会有多个所有者。例如,在图数据结构中,多个边可能指向相同的结点,而这个结点从概念上讲为所有指向它的边所拥有。结点直到没有任何边指向它之前都不应该被清理。
|
||||
|
||||
根据现实生活场景来想象的话,它就像一个客厅的电视。当一个人进来看电视时,他打开电视。其他人也会进来看电视。当最后一个人离开房间时,他关掉电视因为它不再被使用了。如果某人在其他人还在看的时候关掉了电视,正在看电视人肯定会抓狂的!
|
||||
<!-- Can you give an example or two for when a variable needs multiple owners?
|
||||
-->
|
||||
<!-- Done /Carol -->
|
||||
|
||||
`Rc<T>`用于当我们希望在堆上分配一些内存供程序的多个部分读取,而且无法在编译时确定程序的那一部分会最后结束使用它。如果我们知道的话那么常规的所有权规则会在编译时强制起作用。
|
||||
为了启用多所有权,Rust 有一个叫做 `Rc<T>` 的类型。其名称为 **引用计数**(*reference counting*)的缩写。引用计数意味着记录一个值引用的数量来知晓这个值是否仍在被使用。如果某个值有零个引用,就代表没有任何有效引用并可以被清理。
|
||||
|
||||
注意`Rc<T>`只能用于单线程场景;下一章并发会涉及到如何在多线程程序中进行引用计数。如果尝试在多线程中使用`Rc<T>`则会得到一个编译错误。
|
||||
可以将其想象为客厅中的电视。当一个人进来看电视时,他打开电视。其他人也可以进来看电视。当最后一个人离开房间时,他关掉电视因为它不再被使用了。如果某人在其他人还在看的时候就关掉了电视,正在看电视的人肯定会抓狂的!
|
||||
|
||||
### 使用`Rc<T>`分享数据
|
||||
`Rc<T>` 用于当我们希望在堆上分配一些内存供程序的多个部分读取,而且无法在编译时确定程序的那一部分会最后结束使用它的时候。如果确实知道哪部分会结束使用的话,就可以令其成为数据的所有者同时正常的所有权规则就可以在编译时生效。
|
||||
|
||||
让我们回到示例 15-5 中的 cons list 例子。在示例 15-11 中尝试使用`Box<T>`定义的`List`。首先创建了一个包含 5 接着是 10 的列表实例。之后我们想要创建另外两个列表:一个以 3 开始并后接第一个包含 5 和 10 的列表,另一个以 4 开始其后**也**是第一个列表。换句话说,我们希望这两个列表共享第三个列表的所有权,概念上类似于图 15-10:
|
||||
注意 `Rc<T>` 只能用于单线程场景;第十六章并发会涉及到如何在多线程程序中进行引用计数。
|
||||
|
||||
### 使用 `Rc<T>` 共享数据
|
||||
|
||||
让我们回到示例 15-6 中使用 `Box<T>` 定义 cons list 的例子。这一次,我们希望创建两个共享第三个列表所有权的列表,其概念将会看起来如图 15-19 所示:
|
||||
|
||||
<img alt="Two lists that share ownership of a third list" src="img/trpl15-03.svg" class="center" />
|
||||
|
||||
<span class="caption">图 15-10: 两个列表, `b` 和 `c`, 共享第三个列表 `a` 的所有权</span>
|
||||
<span class="caption">图 15-19: 两个列表, `b` 和 `c`, 共享第三个列表 `a` 的所有权</span>
|
||||
|
||||
尝试使用`Box<T>`定义的`List`并不能工作,如示例 15-11 所示:
|
||||
列表 `a` 包含 5 之后是 10,之后是另两个列表:`b` 从 3 开始而 `c` 从 4 开始。`b` 和 `c` 会接上包含 5 和 10 的列表 `a`。换句话说,这两个列表会尝试共享第一个列表所包含的 5 和 10。
|
||||
|
||||
<span class="filename">文件: src/main.rs</span>
|
||||
尝试使用 `Box<T>` 定义的 `List` 并实现不能工作,如示例 15-20 所示:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust,ignore
|
||||
enum List {
|
||||
@ -41,11 +49,11 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 15-11: 具有两个使用 `Box<T>` 的列表,试图共享第三个列表的所有权,是不能正常运行的/span>
|
||||
<span class="caption">示例 15-20: 展示不能用两个 `Box<T>` 的列表尝试共享第三个列表的所有权/span>
|
||||
|
||||
编译会得出如下错误:
|
||||
|
||||
```
|
||||
```text
|
||||
error[E0382]: use of moved value: `a`
|
||||
--> src/main.rs:13:30
|
||||
|
|
||||
@ -58,11 +66,17 @@ error[E0382]: use of moved value: `a`
|
||||
implement the `Copy` trait
|
||||
```
|
||||
|
||||
`Cons`成员拥有其储存的数据,所以当创建`b`列表时将`a`的所有权移动到了`b`。接着当再次尝使用`a`创建`c`时,这不被允许因为`a`的所有权已经被移动。
|
||||
`Cons` 成员拥有其储存的数据,所以当创建 `b` 列表时,`a` 被移动进了 `b` 这样 `b` 就拥有了 `a`。接着当再次尝使用 `a` 创建 `c` 时,这不被允许因为 `a` 的所有权已经被移动。
|
||||
|
||||
相反可以改变`Cons`的定义来存放一个引用,不过接着必须指定生命周期参数,而且在构造列表时,也必须使列表中的每一个元素都至少与列表本身存在的一样久。否则借用检查器甚至都不会允许我们编译代码。
|
||||
可以改变 `Cons` 的定义来存放一个引用,不过接着必须指定生命周期参数。通过指定生命周期参数,表明列表中的每一个元素都至少与列表本身存在的一样久。例如,借用检查器不会允许 `let a = Cons(10, &Nil);` 编译,因为临时值 `Nil` 会在 `a` 获取其引用之前就被丢弃了。
|
||||
|
||||
如示例 15-12 所示,可以将`List`的定义从`Box<T>`改为`Rc<T>`:
|
||||
相反,我们修改 `List` 的定义为使用 `Rc<T>` 代替 `Box<T>`,如列表 15-21 所示。现在每一个 `Cons` 变量都包含一个值和一个指向 `List` 的 `Rc`。当创建 `b` 时,不同于获取 `a` 的所有权,这里会克隆 `a` 所包含的 `Rc`,这会将引用计数从 1 增加到 2 并允许 `a` 和 `b` 共享 `Rc` 中数据的所有权。创建 `c` 时也会克隆 `a`,这会将引用计数从 2 增加为 3。每次调用 `Rc::clone`,`Rc` 中数据的引用计数都会增加,直到有零个引用之前其数据都不会被清理:
|
||||
|
||||
<!-- And what will Rc do that's different here, how will the ownership of a b
|
||||
c change? Could you write a paragraph equivalent to the one describing the cons
|
||||
variants above? That was really useful -->
|
||||
<!-- I'm not sure which paragraph about cons you're talking about, but I've
|
||||
tried to guess /Carol -->
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -77,18 +91,35 @@ use std::rc::Rc;
|
||||
|
||||
fn main() {
|
||||
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
|
||||
let b = Cons(3, a.clone());
|
||||
let c = Cons(4, a.clone());
|
||||
let b = Cons(3, Rc::clone(&a));
|
||||
let c = Cons(4, Rc::clone(&a));
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 15-12: 使用 `Rc<T>` 定义的 `List`</span>
|
||||
<span class="caption">示例 15-21: 使用 `Rc<T>` 定义的 `List`</span>
|
||||
|
||||
注意必须为`Rc`增加`use`语句因为它不在 prelude 中。在`main`中创建了存放 5 和 10 的列表并将其存放在一个叫做`a`的新的`Rc`中。接着当创建`b`和`c`时,我们对`a`调用了`clone`方法。
|
||||
需要为 `Rc` 增加`use`语句因为它不在 prelude 中。在 `main` 中创建了存放 5 和 10 的列表并将其存放在 `a` 的新的 `Rc` 中。接着当创建 `b` 和 `c` 时,调用 `Rc::clone` 函数并传递 `a` 中 `Rc` 的引用作为参数。
|
||||
|
||||
### 克隆`Rc<T>`会增加引用计数
|
||||
也可以调用 `a.clone()` 而不是 `Rc::clone(&a)`,不过在这里 Rust 的习惯是使用 `Rc::clone`。`Rc::clone` 的实现并不像大部分类型的 `clone` 实现那样对所有数据进行深拷贝。`Rc::clone` 只会增加引用计数,这并不会花费多少时间。深拷贝可能会花费很长时间,所以通过使用 `Rc::clone` 进行引用计数,可以明显的区别可能会对运行时性能有巨大影响的深拷贝和不分配内存的对运行时性能影响相对较小的增加引用计数拷贝。
|
||||
|
||||
之前我们见过`clone`方法,当时使用它来创建某些数据的完整拷贝。但是对于`Rc<T>`来说,它并不创建一个完整的拷贝。`Rc<T>`存放了**引用计数**,也就是说,一个存在多少个克隆的计数器。让我们像示例 15-13 那样在创建`c`时增加一个内部作用域,并在不同的位置打印出关联函数`Rc::strong_count`的结果。`Rc::strong_count`返回传递给它的`Rc`值的引用计数,而在本章的稍后部分介绍避免引用循环时讲到它为什么叫做`strong_count`。
|
||||
### 克隆 `Rc<T>` 会增加引用计数
|
||||
|
||||
让我们修改示例 15-21 的代码以便观察创建和丢弃 `a` 中 `Rc` 的引用时引用计数的变化。
|
||||
|
||||
<!-- Below -- can you let the reader know why we are doing this? What does it
|
||||
show us/improve? Is this our working version of the code, or just illustrating
|
||||
reference count? -->
|
||||
<!-- This is illustrating reference counting /Carol -->
|
||||
|
||||
在示例 15-22 中,修改了 `main` 以便将列表 `c` 置于内部作用域中,这样就可以观察当 `c` 离开作用域时引用计数如何变化。在程序中每个引用计数变化的点,会打印出引用计数,其值可以通过调用 `Rc::strong_count` 函数获得。在本章稍后的部分讨论避免引用循环时会解释为何这个函数叫做 `strong_count` 而不是 `count`。
|
||||
|
||||
<!-- If we need to talk about this later, that might indicate that this chapter
|
||||
is out of order --- should the section on reference cycles come first? -->
|
||||
<!-- It's not possible to create reference cycles until we've explained both
|
||||
`Rc` and `RefCell`, so we don't see a way to reorder these sections. The
|
||||
"strong" is the only detail from that section relevant here; we just want to
|
||||
have the reader ignore that detail for now but know that we will explain it in
|
||||
a bit. /Carol -->
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -103,28 +134,34 @@ fn main() {
|
||||
#
|
||||
fn main() {
|
||||
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
|
||||
println!("rc = {}", Rc::strong_count(&a));
|
||||
let b = Cons(3, a.clone());
|
||||
println!("rc after creating b = {}", Rc::strong_count(&a));
|
||||
println!("count after creating a = {}", Rc::strong_count(&a));
|
||||
let b = Cons(3, Rc::clone(&a));
|
||||
println!("count after creating b = {}", Rc::strong_count(&a));
|
||||
{
|
||||
let c = Cons(4, a.clone());
|
||||
println!("rc after creating c = {}", Rc::strong_count(&a));
|
||||
let c = Cons(4, Rc::clone(&a));
|
||||
println!("count after creating c = {}", Rc::strong_count(&a));
|
||||
}
|
||||
println!("rc after c goes out of scope = {}", Rc::strong_count(&a));
|
||||
println!("count after c goes out of scope = {}", Rc::strong_count(&a));
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 15-13: 打印输出引用计数</span>
|
||||
<span class="caption">示例 15-22:打印出引用计数</span>
|
||||
|
||||
这会打印出:
|
||||
|
||||
```
|
||||
rc = 1
|
||||
rc after creating b = 2
|
||||
rc after creating c = 3
|
||||
rc after c goes out of scope = 2
|
||||
```text
|
||||
count after creating a = 1
|
||||
count after creating b = 2
|
||||
count after creating c = 3
|
||||
count after c goes out of scope = 2
|
||||
```
|
||||
|
||||
不难看出`a`的初始引用计数是一。接着每次调用`clone`,计数会加一。当`c`离开作用域时,计数减一,这发生在`Rc<T>`的`Drop` trait 实现中。这个例子中不能看到的是当`b`接着是`a`在`main`函数的结尾离开作用域时,包含 5 和 10 的列表的引用计数会是 0,这时列表将被丢弃。这个策略允许拥有多个所有者,而引用计数会确保任何所有者存在时这个值保持有效。
|
||||
<!-- is there a reason we call `a` rc here, and not just `a`? -->
|
||||
<!-- Yes, because it's not `a`, it's the strong count of the `Rc` in `a`. We've
|
||||
changed the text to hopefully be clearer. /Carol -->
|
||||
|
||||
在本部分的开始,我们说`Rc<T>`只允许程序的多个部分读取`Rc<T>`中`T`的不可变引用。如果`Rc<T>`允许一个可变引用,我们将遇到第四章讨论的借用规则所不允许的问题:两个指向同一位置的可变借用会导致数据竞争和不一致。不过可变数据是非常有用的!在下一部分,我们将讨论内部可变性模式和`RefCell<T>`类型,它可以与`Rc<T>`结合使用来处理不可变性的限制。
|
||||
我们能够看到 `a` 中 `Rc` 的初始引用计数为一,接着每次调用 `clone`,计数会增加一。当 `c` 离开作用域时,计数减一。不必像调用 `Rc::clone` 增加引用计数那样调用一个函数来减少计数;`Drop` trait 的实现当 `Rc` 值离开作用域时自动减少引用计数。
|
||||
|
||||
从这个例子我们所不能看到的是在 `main` 的结尾当 `b` 然后是 `a` 离开作用域时,此处计数会是 0,同时 `Rc` 被完全清理。使用 `Rc` 允许一个值有多个所有者,引用计数则确保只要任何所有者依然存在其值也保持有效。
|
||||
|
||||
`Rc<T>` 允许通过不可变引用来只读的在程序的多个部分共享数据。如果 `Rc<T>` 也允许多个可变引用,则会违反第四章讨论的借用规则之一:相同位置的多个可变借用可能造成数据竞争和不一致。不过可以修改数据是非常有用的!在下一部分,我们将讨论内部可变性模式和 `RefCell<T>` 类型,它可以与 `Rc<T>` 结合使用来处理不可变性的限制。
|
||||
|
@ -1,126 +1,300 @@
|
||||
## `RefCell<T>`和内部可变性模式
|
||||
## `RefCell<T>` 和内部可变性模式
|
||||
|
||||
> [ch15-05-interior-mutability.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-05-interior-mutability.md)
|
||||
> <br>
|
||||
> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
|
||||
> commit 54169ef43f57847913ebec7e021c1267663a5d12
|
||||
|
||||
**内部可变性**(*Interior mutability*)是 Rust 中的一个设计模式,它允许你即使在有不可变引用时改变数据,这通常是借用规则所不允许。内部可变性模式涉及到在数据结构中使用`unsafe`代码来模糊 Rust 通常的可变性和借用规则。我们还未讲到不安全代码;第十九章会学习他们。内部可变性模式用于当你可以确保代码在运行时也会遵守借用规则,哪怕编译器也不能保证的情况。引入的`unsafe`代码将被封装进安全的 API 中,而外部类型仍然是不可变的。
|
||||
<!-- I'm concerned here about referencing forward too much, do we need that
|
||||
information from Ch 19 to understand this? Should we look at rearranging a few
|
||||
things here? -->
|
||||
<!-- We don't think the reader needs to *understand* `unsafe` at this point,
|
||||
just that `unsafe` is how this is possible and that we'll learn about `unsafe`
|
||||
later. After reading this section, did you feel that you needed to know more
|
||||
about `unsafe` to understand this section? /Carol -->
|
||||
|
||||
让我们通过遵循内部可变性模式的`RefCell<T>`类型来开始探索。
|
||||
<!--below: as in, we use the pattern, or it's used automatically? I'm not clear
|
||||
on what's the user's responsibility with this pattern -->
|
||||
<!-- When we choose to use types implemented using the interior mutability
|
||||
pattern, or when we implement our own types using the interior mutability
|
||||
pattern. /Carol -->
|
||||
|
||||
### `RefCell<T>`拥有内部可变性
|
||||
**内部可变性**(*Interior mutability*)是 Rust 中的一个设计模式,它允许你即使在有不可变引用时改变数据,这通常是借用规则所不允许的。为此,该模式在数据结构中使用 `unsafe` 代码来模糊 Rust 通常的可变性和借用规则。我们还未讲到不安全代码;第十九章会学习它们。当可以确保代码在运行时会遵守借用规则,即使编译器不能保证的情况,可以选择使用那些运用内部可变性模式的类型。所涉及的 `unsafe` 代码将被封装进安全的 API 中,而外部类型仍然是不可变的。
|
||||
|
||||
不同于`Rc<T>`,`RefCell<T>`代表其数据的唯一的所有权。那么是什么让`RefCell<T>`不同于像`Box<T>`这样的类型呢?回忆一下第四章所学的借用规则:
|
||||
让我们通过遵循内部可变性模式的 `RefCell<T>` 类型来开始探索。
|
||||
|
||||
1. 在任意给定时间,**只能**拥有如下中的一个:
|
||||
### 通过 `RefCell<T>` 在运行时检查借用规则
|
||||
|
||||
不同于 `Rc<T>`,`RefCell<T>` 代表其数据的唯一的所有权。那么是什么让 `RefCell<T>` 不同于像 `Box<T>` 这样的类型呢?回忆一下第四章所学的借用规则:
|
||||
|
||||
1. 在任意给定时间,**只能** 拥有如下中的一个:
|
||||
* 一个可变引用。
|
||||
* 任意数量的不可变引用。
|
||||
2. 引用必须总是有效的。
|
||||
|
||||
对于引用和`Box<T>`,借用规则的不可变性作用于编译时。对于`RefCell<T>`,这些不可变性作用于**运行时**。对于引用,如果违反这些规则,会得到一个编译错误。而对于`RefCell<T>`,违反这些规则会`panic!`。
|
||||
对于引用和 `Box<T>`,借用规则的不可变性作用于编译时。对于 `RefCell<T>`,这些不可变性作用于 **运行时**。对于引用,如果违反这些规则,会得到一个编译错误。而对于`RefCell<T>`,违反这些规则会 `panic!`。
|
||||
|
||||
Rust 编译器执行的静态分析天生是保守的。代码的一些属性则不可能通过分析代码发现:其中最著名的就是[停机问题(Halting Problem)](https://zh.wikipedia.org/wiki/%E5%81%9C%E6%9C%BA%E9%97%AE%E9%A2%98),这超出了本书的范畴,不过如果你感兴趣的话这是一个值得研究的有趣主题。
|
||||
<!-- Is there an advantage to having these rules enforced at different times?
|
||||
-->
|
||||
<!-- Yes, that's what we were trying to say below, we've tried to make this more explicit /Carol -->
|
||||
|
||||
因为一些分析是不可能的,Rust 编译器在其不确定的时候甚至都不尝试猜测,所以说它是保守的而且有时会拒绝事实上不会违反 Rust 保证的正确的程序。换句话说,如果 Rust 接受不正确的程序,那么人们也就不会相信 Rust 所做的保证了。如果 Rust 拒绝正确的程序,会给程序员带来不便,但不会带来灾难。`RefCell<T>`正是用于当你知道代码遵守借用规则,而编译器不能理解的时候。
|
||||
在编译时检查借用规则的好处是这些错误将在开发过程的早期被捕获同时对没有运行时性能影响,因为所有的分析都提前完成了。为此,在编译时检查借用规则是大部分情况的最佳选择,这也正是其为何是 Rust 的默认行为。
|
||||
|
||||
类似于`Rc<T>`,`RefCell<T>`只能用于单线程场景。在并发章节会介绍如何在多线程程序中使用`RefCell<T>`的功能。现在所有你需要知道的就是如果尝试在多线程上下文中使用`RefCell<T>`,会得到一个编译错误。
|
||||
相反在运行时检查借用规则的好处是特定内存安全的场景是允许的,而它们在编译时检查中是不允许的。静态分析,正如 Rust 编译器,是天生保守的。代码的一些属性则不可能通过分析代码发现:其中最著名的就是 [停机问题(Halting Problem)](https://zh.wikipedia.org/wiki/%E5%81%9C%E6%9C%BA%E9%97%AE%E9%A2%98),这超出了本书的范畴,不过如果你感兴趣的话这是一个值得研究的有趣主题。
|
||||
|
||||
对于引用,可以使用`&`和`&mut`语法来分别创建不可变和可变的引用。不过对于`RefCell<T>`,我们使用`borrow`和`borrow_mut`方法,它是`RefCell<T>`拥有的安全 API 的一部分。`borrow`返回`Ref`类型的智能指针,而`borrow_mut`返回`RefMut`类型的智能指针。这两个类型实现了`Deref`所以可以被当作常规引用处理。`Ref`和`RefMut`动态的借用所有权,而他们的`Drop`实现也动态的释放借用。
|
||||
<!--below: can't be sure of what, exactly? Sure that the code complies with the
|
||||
ownership rules? -->
|
||||
<!-- Yes /Carol -->
|
||||
|
||||
示例 15-14 展示了如何使用`RefCell<T>`来使函数不可变的和可变的借用它的参数。注意`data`变量使用`let data`而不是`let mut data`来声明为不可变的,而`a_fn_that_mutably_borrows`则允许可变的借用数据并修改它!
|
||||
因为一些分析是不可能的,如果 Rust 编译器不能通过所有权规则编译,它可能会拒绝一个正确的程序;从这种角度考虑它是保守的。如果 Rust 接受不正确的程序,那么人们也就不会相信 Rust 所做的保证了。然而,如果 Rust 拒绝正确的程序,会给程序员带来不便,但不会带来灾难。`RefCell<T>` 正是用于当你确信代码遵守借用规则,而编译器不能理解和确定的时候。
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
类似于 `Rc<T>`,`RefCell<T>` 只能用于单线程场景。如果尝试在多线程上下文中使用`RefCell<T>`,会得到一个编译错误。第十六章会介绍如何在多线程程序中使用 `RefCell<T>` 的功能。
|
||||
|
||||
```rust
|
||||
use std::cell::RefCell;
|
||||
<!-- I'm not really clear at this point what the difference between Rc<T> and
|
||||
RefCell<T> is, perhaps a succinct round up would help? -->
|
||||
<!-- Done /Carol -->
|
||||
|
||||
fn a_fn_that_immutably_borrows(a: &i32) {
|
||||
println!("a is {}", a);
|
||||
}
|
||||
如下为选择 `Box<T>`,`Rc<T>` 或 `RefCell<T>` 的理由:
|
||||
|
||||
fn a_fn_that_mutably_borrows(b: &mut i32) {
|
||||
*b += 1;
|
||||
}
|
||||
- `Rc<T>` 允许相同数据有多个所有者;`Box<T>` 和 `RefCell<T>` 有单一所有者。
|
||||
- `Box<T>` 允许在编译时检查的不可变或可变借用;`RefCell<T>` 允许在运行时价差的不可变或可变借用
|
||||
- 因为 `RefCell<T>` 允许在运行时检查的可变借用,可以在即便 `RefCell<T>` 自身是不可变的情况下修改其内部的值。
|
||||
|
||||
fn demo(r: &RefCell<i32>) {
|
||||
a_fn_that_immutably_borrows(&r.borrow());
|
||||
a_fn_that_mutably_borrows(&mut r.borrow_mut());
|
||||
a_fn_that_immutably_borrows(&r.borrow());
|
||||
}
|
||||
最有一个理由便是指 **内部可变性** 模式。让我们看看何时内部可变性是有用的,并讨论这是如何成为可能的。
|
||||
|
||||
fn main() {
|
||||
let data = RefCell::new(5);
|
||||
demo(&data);
|
||||
}
|
||||
```
|
||||
### 内部可变性:不可变值的可变借用
|
||||
|
||||
<span class="caption">示例 15-14: 使用 `RefCell<T>`, `borrow` 和
|
||||
`borrow_mut`</span>
|
||||
|
||||
这个例子打印出:
|
||||
|
||||
```
|
||||
a is 5
|
||||
a is 6
|
||||
```
|
||||
|
||||
在`main`函数中,我们新声明了一个包含值 5 的`RefCell<T>`,并储存在变量`data`中,声明时并没有使用`mut`关键字。接着使用`data`的一个不可变引用来调用`demo`函数:对于`main`函数而言`data`是不可变的!
|
||||
|
||||
在`demo`函数中,通过调用`borrow`方法来获取到`RefCell<T>`中值的不可变引用,并使用这个不可变引用调用了`a_fn_that_immutably_borrows`函数。更为有趣的是,可以通过`borrow_mut`方法来获取`RefCell<T>`中值的**可变**引用,而`a_fn_that_mutably_borrows`函数就允许修改这个值。可以看到下一次调用`a_fn_that_immutably_borrows`时打印出的值是 6 而不是 5。
|
||||
|
||||
### `RefCell<T>`在运行时检查借用规则
|
||||
|
||||
回忆一下第四章因为借用规则,尝试使用常规引用在同一作用域中创建两个可变引用的代码无法编译:
|
||||
借用规则的一个推论是当有一个不可变值时,不能可变的借用它。例如,如下代码不能编译:
|
||||
|
||||
```rust,ignore
|
||||
let mut s = String::from("hello");
|
||||
|
||||
let r1 = &mut s;
|
||||
let r2 = &mut s;
|
||||
```
|
||||
|
||||
这会得到一个编译错误:
|
||||
|
||||
```
|
||||
error[E0499]: cannot borrow `s` as mutable more than once at a time
|
||||
-->
|
||||
|
|
||||
5 | let r1 = &mut s;
|
||||
| - first mutable borrow occurs here
|
||||
6 | let r2 = &mut s;
|
||||
| ^ second mutable borrow occurs here
|
||||
7 | }
|
||||
| - first borrow ends here
|
||||
```
|
||||
|
||||
与此相反,使用`RefCell<T>`并在同一作用域调用两次`borrow_mut`的代码是**可以**编译的,不过它会在运行时 panic。如下代码:
|
||||
|
||||
```rust,should_panic
|
||||
use std::cell::RefCell;
|
||||
|
||||
fn main() {
|
||||
let s = RefCell::new(String::from("hello"));
|
||||
|
||||
let r1 = s.borrow_mut();
|
||||
let r2 = s.borrow_mut();
|
||||
let x = 5;
|
||||
let y = &mut x;
|
||||
}
|
||||
```
|
||||
|
||||
能够编译不过在`cargo run`运行时会出现如下错误:
|
||||
如果尝试编译,会得到如下错误:
|
||||
|
||||
```text
|
||||
error[E0596]: cannot borrow immutable local variable `x` as mutable
|
||||
--> src/main.rs:3:18
|
||||
|
|
||||
2 | let x = 5;
|
||||
| - consider changing this to `mut x`
|
||||
3 | let y = &mut x;
|
||||
| ^ cannot borrow mutably
|
||||
```
|
||||
Finished dev [unoptimized + debuginfo] target(s) in 0.83 secs
|
||||
Running `target/debug/refcell`
|
||||
thread 'main' panicked at 'already borrowed: BorrowMutError',
|
||||
/stable-dist-rustc/build/src/libcore/result.rs:868
|
||||
|
||||
然而,特定情况下在值的方法内部能够修改自身是很有用的;而不是在其他代码中,此时值仍然是不可变。值方法外部的代码不能修改其值。`RefCell<T>` 是一个获得内部可变性的方法。`RefCell<T>` 并没有完全绕开借用规则,编译器中的借用检查器允许内部可变性并相应的在运行时检查借用规则。如果违反了这些规则,会得到 `panic!` 而不是编译错误。
|
||||
|
||||
让我们通过一个实际的例子来探索何处可以使用 `RefCell<T>` 来修改不可变值并看看为何这么做是有意义的。
|
||||
|
||||
#### 内部可变性的用例:mock 对象
|
||||
|
||||
**测试替身**(*test double*)是一个通用编程概念,它代表一个在测试中替代某个类型的类型。**mock 对象** 是特定类型的测试替身,它们记录测试过程中发生了什么以便可以断言操作是正确的。
|
||||
|
||||
虽然 Rust 没有与其他语言中的对象完全相同的对象,Rust 也没有像其他语言那样在标准库中内建 mock 对象功能,不过我们确实可以创建一个与 mock 对象有着相同功能的结构体。
|
||||
|
||||
如下是一个我们想要测试的场景:我们在编写一个记录某个值与最大值的差距的库,并根据当前值与最大值的差距来发送消息。例如,这个库可以用于记录用户所允许的 API 调用数量限额。
|
||||
|
||||
该库只提供记录与最大值的差距,以及何种情况发送什么消息的功能。使用此库的程序则期望提供实际发送消息的机制:程序可以选择记录一条消息、发送 email、发送短信等等。库本身无需知道这些细节;只需实现其提供的 `Messenger` trait 即可。示例 15-23 展示了库代码:
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
pub trait Messenger {
|
||||
fn send(&self, msg: &str);
|
||||
}
|
||||
|
||||
pub struct LimitTracker<'a, T: 'a + Messenger> {
|
||||
messenger: &'a T,
|
||||
value: usize,
|
||||
max: usize,
|
||||
}
|
||||
|
||||
impl<'a, T> LimitTracker<'a, T>
|
||||
where T: Messenger {
|
||||
pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {
|
||||
LimitTracker {
|
||||
messenger,
|
||||
value: 0,
|
||||
max,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_value(&mut self, value: usize) {
|
||||
self.value = value;
|
||||
|
||||
let percentage_of_max = self.value as f64 / self.max as f64;
|
||||
|
||||
if percentage_of_max >= 0.75 && percentage_of_max < 0.9 {
|
||||
self.messenger.send("Warning: You've used up over 75% of your quota!");
|
||||
} else if percentage_of_max >= 0.9 && percentage_of_max < 1.0 {
|
||||
self.messenger.send("Urgent warning: You've used up over 90% of your quota!");
|
||||
} else if percentage_of_max >= 1.0 {
|
||||
self.messenger.send("Error: You are over your quota!");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 15-23:一个记录某个值与最大值差距的库,并根据此值的特定级别发出警告</span>
|
||||
|
||||
这些代码中一个重要部分是拥有一个方法 `send` 的 `Messenger` trait,其获取一个 `self` 的可变引用和文本信息。这是我们的 mock 对象所需要拥有的接口。另一个重要的部分是我们需要测试 `LimitTracker` 的 `set_value` 方法的行为。可以改变传递的 `value` 参数的值,不过 `set_value` 并没有返回任何可供断言的值。也就是说,如果使用某个实现了 `Messenger` trait 的值和特定的 `max` 创建 `LimitTracker`,当传递不同 `value` 值时,消息发送者应被告知发送合适的消息。
|
||||
|
||||
我们所需的 mock 对象是,调用 `send` 不同于实际发送 email 或短息,其只记录信息被通知要发送了。可以新建一个 mock 对象示例,用其创建 `LimitTracker`,调用 `LimitTracker` 的 `set_value` 方法,然后检查 mock 对象是否有我们期望的消息。示例 15-24 展示了一个如此尝试的 mock 对象实现,不过借用检查器并不允许:
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
struct MockMessenger {
|
||||
sent_messages: Vec<String>,
|
||||
}
|
||||
|
||||
impl MockMessenger {
|
||||
fn new() -> MockMessenger {
|
||||
MockMessenger { sent_messages: vec![] }
|
||||
}
|
||||
}
|
||||
|
||||
impl Messenger for MockMessenger {
|
||||
fn send(&self, message: &str) {
|
||||
self.sent_messages.push(String::from(message));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_sends_an_over_75_percent_warning_message() {
|
||||
let mock_messenger = MockMessenger::new();
|
||||
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
|
||||
|
||||
limit_tracker.set_value(80);
|
||||
|
||||
assert_eq!(mock_messenger.sent_messages.len(), 1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 15-24:尝试实现 `MockMessenger`,借用检查器并不允许</span>
|
||||
|
||||
测试代码定义了一个 `MockMessenger` 结构体,其 `sent_messages` 字段为一个 `String` 值的 `Vec` 用来记录被告知发送的消息。我们还定义了一个关联函数 `new` 以便于新建从空消息列表开始的 `MockMessenger` 值。接着为 `MockMessenger` 实现 `Messenger` trait 这样就可以为 `LimitTracker` 提供一个 `MockMessenger`。在 `send` 方法的定义中,获取传入的消息作为参数并储存在 `MockMessenger` 的 `sent_messages` 列表中。
|
||||
|
||||
在测试中,我们测试了当 `LimitTracker` 被告知将 `value` 设置为超过 `max` 值 75% 的某个值。首先新建一个 `MockMessenger`,其从空消息列表开始。接着新建一个 `LimitTracker` 并传递新建 `MockMessenger` 的引用和 `max` 值 100。我们使用值 80 调用 `LimitTracker` 的 `set_value` 方法,这超过了 100 的 75%。接着断言 `MockMessenger` 中记录的消息列表应该有一条消息。
|
||||
|
||||
然而,这个测试是有问题的:
|
||||
|
||||
```text
|
||||
error[E0596]: cannot borrow immutable field `self.sent_messages` as mutable
|
||||
--> src/lib.rs:46:13
|
||||
|
|
||||
45 | fn send(&self, message: &str) {
|
||||
| ----- use `&mut self` here to make mutable
|
||||
46 | self.sent_messages.push(String::from(message));
|
||||
| ^^^^^^^^^^^^^^^^^^ cannot mutably borrow immutable field
|
||||
```
|
||||
|
||||
不能修改 `MockMessenger` 来记录消息,因为 `send` 方法获取 `self` 的不可变引用。我们也不能参考错误文本的建议使用 `&mut self` 替代,因为这样 `send` 的签名就不符合 `Messenger` trait 定义中的签名了(请随意尝试如此修改并看看会出现什么错误信息)。
|
||||
|
||||
这正是内部可变性的用武之地!我们将通过 `RefCell` 来储存 `sent_messages`,然而 `send` 将能够修改 `sent_messages` 并储存消息。示例 15-25 展示了代码:
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::cell::RefCell;
|
||||
|
||||
struct MockMessenger {
|
||||
sent_messages: RefCell<Vec<String>>,
|
||||
}
|
||||
|
||||
impl MockMessenger {
|
||||
fn new() -> MockMessenger {
|
||||
MockMessenger { sent_messages: RefCell::new(vec![]) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Messenger for MockMessenger {
|
||||
fn send(&self, message: &str) {
|
||||
self.sent_messages.borrow_mut().push(String::from(message));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_sends_an_over_75_percent_warning_message() {
|
||||
// --snip--
|
||||
# let mock_messenger = MockMessenger::new();
|
||||
# let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
|
||||
# limit_tracker.set_value(75);
|
||||
|
||||
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 15-25:使用 `RefCell<T>` 能够在外部值被认为是不可变的情况下修改内部值</span>
|
||||
|
||||
现在 `sent_messages` 字段的类型是 `RefCell<Vec<String>>` 而不是 `Vec<String>`。在 `new` 函数中新建了一个 `RefCell` 示例替代空 vector。
|
||||
|
||||
对于 `send` 方法的实现,第一个参数仍为 `self` 的不可变借用,这是符合方法定义的。我们调用 `self.sent_messages` 中 `RefCell` 的 `borrow_mut` 方法来获取 `RefCell` 中值的可变引用,这是一个 vector。接着可以对 vector 的可变引用调用 `push` 以便记录测试过程中看到的消息。
|
||||
|
||||
最后必须做出的修改位于断言中:为了看到其内部 vector 中有多少个项,需要调用 `RefCell` 的 `borrow` 以获取 vector 的不可变引用。
|
||||
|
||||
现在我们见识了如何使用 `RefCell<T>`,让我们研究一下它怎样工作的!
|
||||
|
||||
### `RefCell<T>` 在运行时检查借用规则
|
||||
|
||||
当创建不可变和可变引用时,我们分别使用 `&` 和 `&mut` 语法。对于 `RefCell<T>` 来说,则是 `borrow` 和 `borrow_mut` 方法,这属于 `RefCell<T>` 安全 API 的一部分。`borrow` 方法返回 `Ref` 类型的智能指针,`borrow_mut` 方法返回 `RefMut` 类型的智能指针。这两个类型都实现了 `Deref` 所以可以当作常规引用对待。
|
||||
|
||||
<!-- can you clarify what you mean, practically, by "track borrows
|
||||
dynamically"?-->
|
||||
<!-- Yep, we've tried to clarify in the next paragraph. /Carol -->
|
||||
|
||||
`RefCell<T>` 记录当前有多少个活动的 `Ref` 和 `RefMut` 智能指针。每次调用 `borrow`,`RefCell<T>` 将活动的不可变借用计数加一。当 `Ref` 值离开作用域时,不可变借用计数减一。就像编译时借用规则一样,`RefCell<T>` 在任何时候只允许有多个不可变借用或一个可变借用。
|
||||
|
||||
如果我们尝试违反这些规则,相比引用时的编译时错误,`RefCell<T>` 的实现会在运行时 `panic!`。示例 15-26 展示了对示例 15-25 中 `send` 实现的修改,这里我们故意尝试在相同作用域创建两个可变借用以便演示 `RefCell<T>` 不允许我们在运行时这么做:
|
||||
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust,ignore
|
||||
impl Messenger for MockMessenger {
|
||||
fn send(&self, message: &str) {
|
||||
let mut one_borrow = self.sent_messages.borrow_mut();
|
||||
let mut two_borrow = self.sent_messages.borrow_mut();
|
||||
|
||||
one_borrow.push(String::from(message));
|
||||
two_borrow.push(String::from(message));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 15-26:在同一作用域中创建连个可变引用并观察 `RefCell<T>` panic</span>
|
||||
|
||||
这里为 `borrow_mut` 返回的 `RefMut` 智能指针创建了 `one_borrow` 变量。接着用相同的方式在变量 `two_borrow` 创建了另一个可变借用。这会在相同作用域中创建一个可变引用,这是不允许的,如果运行库的测试,编译时不会有任何错误,不过测试会失败:
|
||||
|
||||
```text
|
||||
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
|
||||
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at
|
||||
'already borrowed: BorrowMutError', src/libcore/result.rs:906:4
|
||||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||||
```
|
||||
|
||||
这个运行时`BorrowMutError`类似于编译错误:它表明我们已经可变得借用过一次`s`了,所以不允许再次借用它。我们并没有绕过借用规则,只是选择让 Rust 在运行时而不是编译时执行他们。你可以选择在任何时候任何地方使用`RefCell<T>`,不过除了不得不编写很多`RefCell`之外,最终还是可能会发现其中的问题(可能是在生产环境而不是开发环境)。另外,在运行时检查借用规则有性能惩罚。
|
||||
可以看到代码 panic 和信息`already borrowed: BorrowMutError`。这也就是 `RefCell<T>` 如何在运行时处理违反借用规则的情况。
|
||||
|
||||
### 结合`Rc<T>`和`RefCell<T>`来拥有多个可变数据所有者
|
||||
在运行时捕获借用错误而不是编译时意味着将会在开发过程的后期才会发现错误 ———— 甚至有可能发布到生产环境才发现。还会因为在运行时而不是编译时记录借用而导致少量的运行时性能惩罚。然而,使用 `RefCell` 使得在只允许不可变值的上下文中编写修改自身以记录消息的 mock 对象成为可能。虽然有取舍,但是我们可以选择使用 `RefCell<T>` 来获得比常规引用所能提供的更多的功能。
|
||||
|
||||
那么为什么要权衡考虑选择引入`RefCell<T>`呢?好吧,还记得我们说过`Rc<T>`只能拥有一个`T`的不可变引用吗?考虑到`RefCell<T>`是不可变的,但是拥有内部可变性,可以将`Rc<T>`与`RefCell<T>`结合来创造一个既有引用计数又可变的类型。示例 15-15 展示了一个这么做的例子,再次回到示例 15-5 中的 cons list。在这个例子中,不同于在 cons list 中储存`i32`值,我们储存一个`Rc<RefCell<i32>>`值。希望储存这个类型是因为其可以拥有不属于列表一部分的这个值的所有者(`Rc<T>`提供的多个所有者功能),而且还可以改变内部的`i32`值(`RefCell<T>`提供的内部可变性功能):
|
||||
### 结合 `Rc<T>` 和 `RefCell<T>` 来拥有多个可变数据所有者
|
||||
|
||||
`RefCell<T>` 的一个常见用法是与 `Rc<T>` 结合。回忆一下 `Rc<T>` 允许对相同数据有多个所有者,不过只能提供数据的不可变访问。如果有一个储存了 `RefCell<T>` 的 `Rc<T>` 的话,就可以得到有多个所有者 **并且** 可以修改的值了!
|
||||
|
||||
<!-- maybe just recap on why we'd want that? -->
|
||||
<!-- done, below /Carol -->
|
||||
|
||||
例如,回忆示例 15-13 的 cons list 的例子中使用 `Rc<T>` 使得多个列表共享另一个列表的所有权。因为 `Rc<T>` 只存放不可变值,所以一旦创建了这些列表值后就不能修改。让我们加入 `RefCell<T>` 来获得修改列表中值的能力。示例 15-27 展示了通过在 `Cons` 定义中使用 `RefCell<T>`,我们就允许修改所有列表中的值了:
|
||||
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
@ -138,34 +312,39 @@ use std::cell::RefCell;
|
||||
fn main() {
|
||||
let value = Rc::new(RefCell::new(5));
|
||||
|
||||
let a = Cons(value.clone(), Rc::new(Nil));
|
||||
let shared_list = Rc::new(a);
|
||||
let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
|
||||
|
||||
let b = Cons(Rc::new(RefCell::new(6)), shared_list.clone());
|
||||
let c = Cons(Rc::new(RefCell::new(10)), shared_list.clone());
|
||||
let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
|
||||
let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));
|
||||
|
||||
*value.borrow_mut() += 10;
|
||||
|
||||
println!("shared_list after = {:?}", shared_list);
|
||||
println!("a after = {:?}", a);
|
||||
println!("b after = {:?}", b);
|
||||
println!("c after = {:?}", c);
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">示例 15-15: 使用 `Rc<RefCell<i32>>` 创建能够修改的 `List`</span>
|
||||
<span class="caption">示例 15-27:使用 `Rc<RefCell<i32>>` 创建可以修改的 `List`</span>
|
||||
|
||||
我们创建了一个值,它是`Rc<RefCell<i32>>`的实例。将其储存在变量`value`中因为我们希望之后能直接访问它。接着在`a`中创建了一个拥有存放了`value`值的`Cons`成员的`List`,而且`value`需要被克隆因为我们希望除了`a`之外还拥有`value`的所有权。接着将`a`封装进`Rc<T>`中这样就可以创建都引用`a`的有着不同开头的列表`b`和`c`,类似示例 15-12 中所做的那样。
|
||||
这里创建了一个 `Rc<RefCell<i32>` 实例并储存在变量 `value` 中以便之后直接访问。接着在 `a` 中用包含 `value` 的 `Cons` 成员创建了一个 `List`。需要克隆 `value` 以便 `a` 和 `value` 都能拥有其内部值 `5` 的所有权,而不是将所有权从 `value` 移动到 `a` 或者让 `a` 借用 `value`。
|
||||
|
||||
一旦创建了`shared_list`、`b`和`c`,接下来就可以通过解引用`Rc<T>`和对`RefCell`调用`borrow_mut`来将 10 与 5 相加了。
|
||||
<!-- above: so that `value` has ownership of what, in addition to a? I didn't
|
||||
follow the final sentence above -->
|
||||
<!-- Of the inner value, I've tried to clarify /Carol -->
|
||||
|
||||
当打印出`shared_list`、`b`和`c`时,可以看到他们都拥有被修改的值 15:
|
||||
我们将列表 `a` 封装进了 `Rc<T>` 这样当创建列表 `b` 和 `c` 时,他们都可以引用 `a`,正如示例 15-13 一样。
|
||||
|
||||
```
|
||||
shared_list after = Cons(RefCell { value: 15 }, Nil)
|
||||
一旦创建了列表 `a`、`b` 和 `c`,我们将 `value` 的值加 10。为此对 `value` 调用了 `borrow_mut`,这里使用了第五章讨论的自定解引用功能(“`->`运算符到哪去了?”)来解引用 `Rc<T>` 以获取其内部的 `RefCell<T>` 值。`borrow_mut` 方法返回 `RefMut<T>` 智能指针,可以对其使用解引用运算符并修改其内部值。
|
||||
|
||||
当我们打印出 `a`、`b` 和 `c` 时,可以看到他们都拥有修改后的值 15 而不是 5:
|
||||
|
||||
```text
|
||||
a after = Cons(RefCell { value: 15 }, Nil)
|
||||
b after = Cons(RefCell { value: 6 }, Cons(RefCell { value: 15 }, Nil))
|
||||
c after = Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil))
|
||||
```
|
||||
|
||||
这是非常巧妙的!通过使用`RefCell<T>`,我们可以拥有一个表面上不可变的`List`,不过可以使用`RefCell<T>`中提供内部可变性的方法来在需要时修改数据。`RefCell<T>`的运行时借用规则检查也确实保护我们免于出现数据竞争,而且我们也决定牺牲一些速度来换取数据结构的灵活性。
|
||||
这是非常巧妙的!通过使用 `RefCell<T>`,我们可以拥有一个表面上不可变的 `List`,不过可以使用 `RefCell<T>` 中提供内部可变性的方法来在需要时修改数据。`RefCell<T>` 的运行时借用规则检查也确实保护我们免于出现数据竞争,而且我们也决定牺牲一些速度来换取数据结构的灵活性。
|
||||
|
||||
`RefCell<T>`并不是标准库中唯一提供内部可变性的类型。`Cell<T>`有点类似,不过不同于`RefCell<T>`那样提供内部值的引用,其值被拷贝进和拷贝出`Cell<T>`。`Mutex<T>`提供线程间安全的内部可变性,下一章并发会讨论它的应用。请查看标准库来获取更多细节和不同类型的区别。
|
||||
标准库中也有其他提供内部可变性的类型,比如 `Cell<T>`,它有些类似(`RefCell<T>`)除了相比提供内部值的引用,其值被拷贝进和拷贝出 `Cell<T>`。还有 `Mutex<T>`,其提供线程间安全的内部可变性,下一章并发会讨论它的应用。请查看标准库来获取更多细节和不同类型之间的区别。
|
||||
|
Loading…
Reference in New Issue
Block a user