diff --git a/projects/borrowing/Cargo.toml b/projects/borrowing/Cargo.toml new file mode 100644 index 0000000..c172b53 --- /dev/null +++ b/projects/borrowing/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "borrowing" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/projects/borrowing/src/main.rs b/projects/borrowing/src/main.rs new file mode 100644 index 0000000..8f48d41 --- /dev/null +++ b/projects/borrowing/src/main.rs @@ -0,0 +1,4 @@ +fn main() { + let x = 5; + let y = &mut x; +} diff --git a/projects/cons-list/src/main.rs b/projects/cons-list/src/main.rs index 47c33e4..618d2d2 100644 --- a/projects/cons-list/src/main.rs +++ b/projects/cons-list/src/main.rs @@ -1,12 +1,24 @@ +#[derive(Debug)] enum List { - Cons(i32, Box), + Cons(Rc>, Rc), Nil, } use crate::List::{Cons, Nil}; +use std::cell::RefCell; +use std::rc::Rc; fn main() { - let a = Cons(5, Box::new(Cons(10, Box::new(Nil)))); - let b = Cons(3, Box::new(a)); - let c = Cons(4, Box::new(a)); + let value = Rc::new(RefCell::new(5)); + + let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil))); + + let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a)); + let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a)); + + *value.borrow_mut() += 10; + + println!("a 随后 = {a:?}"); + println!("b 随后 = {b:?}"); + println!("c 随后 = {c:?}"); } diff --git a/projects/limit-tracker/Cargo.toml b/projects/limit-tracker/Cargo.toml new file mode 100644 index 0000000..f5e2eb1 --- /dev/null +++ b/projects/limit-tracker/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "limit-tracker" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/projects/limit_tracker/src/lib.rs b/projects/limit-tracker/src/lib.rs similarity index 57% rename from projects/limit_tracker/src/lib.rs rename to projects/limit-tracker/src/lib.rs index 874a036..8da3408 100644 --- a/projects/limit_tracker/src/lib.rs +++ b/projects/limit-tracker/src/lib.rs @@ -25,15 +25,19 @@ where let percentage_of_max = self.value as f64 / self.max as f64; - match percentage_of_max { - p if p >= 1.0 => self.messenger.send("出错:你已超出你的配额!"), - p if p >= 0.9 => self.messenger.send("紧急警告:你已用掉你配额的 90% !"), - p if p >= 0.75 => self.messenger.send("警告:你已用掉你配额的 75% !"), - _ => {}, - } + if percentage_of_max >= 1.0 { + self.messenger.send("Error: 你已超出配额!"); + } else if percentage_of_max >= 0.9 { + self.messenger + .send("Urgent warning: 你已用完 90% 的配额!"); + } else if percentage_of_max >= 0.75 { + self.messenger + .send("Warning: 你已用完 75% 的配额!"); + } } } + #[cfg(test)] mod tests { use super::*; @@ -46,25 +50,28 @@ mod tests { impl MockMessenger { fn new() -> MockMessenger { MockMessenger { - sent_messages: RefCell::new(vec! []), + sent_messages: RefCell::new(vec![]), } } } impl Messenger for MockMessenger { fn send(&self, message: &str) { - self.sent_messages.borrow_mut().push(String::from(message)); + 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)); } } #[test] - fn it_sends_an_over_75_percent_waring_message() { + 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); - println! ("{}", mock_messenger.sent_messages.borrow().iter().next().unwrap()); - assert_eq! (mock_messenger.sent_messages.borrow().len(), 1); + assert_eq!(mock_messenger.sent_messages.borrow().len(), 1); } } diff --git a/projects/limit_tracker/Cargo.toml b/projects/limit_tracker/Cargo.toml deleted file mode 100644 index c7089a1..0000000 --- a/projects/limit_tracker/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "limit_tracker" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 65fa3f4..8894542 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -94,7 +94,7 @@ - [使用 `Box` 指向堆上数据](smart_pointers/box-t.md) - [将灵巧指针视为普通引用](smart_pointers/deref-t.md) - [通过 `Drop` 特质在清理时运行代码](smart_pointers/drop-t.md) - - [`RC`,引用计数的灵巧指针](smart_pointers/rc-t.md) + - [`Rc`,引用计数的灵巧指针](smart_pointers/rc-t.md) - [`RefCell` 与内部可变性模式](smart_pointers/refcell-t.md) - [引用循环会泄露内存](smart_pointers/ref-cycles.md) diff --git a/src/smart_pointers/refcell-t.md b/src/smart_pointers/refcell-t.md index 881dff5..ea7c46c 100644 --- a/src/smart_pointers/refcell-t.md +++ b/src/smart_pointers/refcell-t.md @@ -1,50 +1,46 @@ # `RefCell` 与内部可变性模式 -**`RefCell` and the Interior Mutability Pattern** +所谓 *内部可变性,interior mutability*,属于 Rust 中的一种设计模式,在即使存在对数据的不可变引用时,也允许咱们修改该数据;通常情况下,这种操作是借用规则所不允许的。为了修改数据,这种模式在数据结构内部使用 `unsafe` 代码,绕过 Rust 管理修改和借用的通常规则。不安全代码向编译器表明,我们正在手动检查规则,而非依赖编译器为咱们检查;我们将在第 20 章中进一步讨论不安全代码。 -*内部可变性,interior mutability* 属于 Rust 中的一种设计模式,他实现了即使在有着到数据的一些不可变引用之下,对数据加以改变;一般情况下,这样的行为是借用规则所不允许的。为了改变数据,这种模式便运用了数据结构内部的一些 `unsafe` 代码,来改变了 Rust 监管可变性与借用的一些一般规则。这些不安全代码向编译器表明,咱们自己在手动检查那些规则,而非依赖于编译器为咱们检查那些规则;在第 19 章将进一步讨论这些不安全代码。 +尽管编译器无法保证借用规则在运行时会得以遵守,但只要我们可以确保这点,我们就可以使用用到内部可变性模式的类型。此时,所涉及的不安全代码会被封装于安全的 API 中,而外层类型仍然是不可变的。 -只有当我们可以确保在运行时遵循借用规则时,我们才能使用使用内部可变性模式的类型,即使编译器不能保证这一点。然后将涉及的不安全代码包装在安全的 API 中,并且外部类型仍然是不可变的。 - -咱们来通过检视遵循内部可变性模式的 `RefCell` 类型来探讨这个概念。 +咱们来通过研究遵循内部可变性模式的 `RefCell` 类型,探讨这一概念。 -## 使用 `RefCell` 在运行时执行借用规则检查 +## 在运行时强制执行借用规则检查 -**Enforcing Borrowing Rules at Runtime with `RefCell`** +与 `Rc` 不同,`RefCell` 类型表示对其包含的数据的单一所有权。那么,是什么使 `RefCell` 不同于 `Box` 这样的类型呢?回顾咱们在第 4 章中学到的 [借用规则](../ownership/references_and_borrowing.md#引用的规则): - -与 `Rc` 不同,`RefCell` 类型表示对其所持有的数据的单一所有权。那么,是什么使 `RefCell` 与 `Box` 这样的类型不同呢?回顾咱们在第四章学到的借用规则: - -- 在任何给定时间,咱们都可以有着 *要么* (而非同时) 一个可变引用,要么任意数量的不可变引用; +- 在任何给定时间,咱们都可以 *要么* 有着一个可变引用,*或者* 任意数量的不可变引用(但不能两者兼有); - 引用必须始终有效。 +在引用与 `Box` 下,借用规则的这两项不变性在编译时强制执行。而在 `RefCell` 下,这两项不变性在运行时强制执行。对于引用,当咱们破坏了这两条规则,咱们将得到编译器报错。而在 `RefCell` 下,当咱们破坏这两条规则时,咱们的程序将终止运行并退出。 -对于引用与 `Box`,借用规则的不变性,the borrowing rules' invariants, 是在编译时强制执行的。对于 `RefCell`,这些不变性则是在运行时强制执行的。对于引用,如果咱们破坏了这些规则,咱们会得到编译器报错。而在 `RefCell` 中,如果咱们破坏了这些规则,咱们的程序将终止运行。 +在编译时检查借用规则的好处是,错误将在开发过程中被尽早捕获,并且由于所有分析都已提前完成,而没有对运行时性能的影响。出于这些原因,在编译时检查借用规则在大多数情况下都是最佳选择,这就是为什么这是 Rust 的默认设置。 -在编译时检查借用规则的好处是在开发过程中会更早地发现错误,而且对运行时性能没有影响,因为所有分析都是事先完成的。由于这些原因,在大多数情况下,在编译时检查借用规则是最好的选择,这就是为什么这是 Rust 的默认设置。 +相反,在运行时检查借用规则的优势在于,此时某些内存安全的场景被允许,而这些场景不会被编译时的检查所允许。像 Rust 编译器这样的静态分析,本质上是保守的。代码的某些属性是无法通过分析代码来检测的:最著名的例子就是 [停机问题](https://zh.wikipedia.org/wiki/%E5%81%9C%E6%9C%BA%E9%97%AE%E9%A2%98), 虽然这超出了本书的范围,但却是个值得研究的有趣主题。 -相反,在运行时检查借用规则的优点是允许某些内存安全的场景,而编译时检查则不会允许这些场景。与 Rust 编译器一样,静态分析,static analysis,本质上是保守的。代码的某些属性无法通过分析代码来检测:最著名的例子是停机问题,the Halting Problem, 它超出了本书的范围,但却是一个值得研究的有趣主题。 +由于有些分析是不可能的,当 Rust 编译器无法确定代码是否符合所有权规则时,他可能会拒绝某个正确的程序;从这方面讲,他是保守的。若 Rust 编译器接受了某个错误的程序,用户就无法信任 Rust 做出的保证。然而,当 Rust 拒绝某个正确的程序时,虽然程序员会感到不便,但不会发生灾难性的后果。当咱们确信代码遵循了借用规则,而编译器却无法理解和保证这点时,`RefCell` 类型非常有用。 -由于某些分析是不可行的,那么如果 Rust 编译器不能确定代码符合所有权规则,他可能会拒绝某个正确的程序;从这方面讲,他是保守的。如果 Rust 编译器接受了错误的程序,用户就无法相信 Rust 做出的保证。然而,如果 Rust 拒绝了某个正确的程序,编程者会感到不便,但又不会发生什么灾难性的事情。在咱们确定咱们的代码遵循借用规则,而编译器无法理解和保证时,`RefCell` 类型就很有用。 +与 `Rc` 类似,`RefCell` 仅适用于单线程的场景,并会在咱们试图于多线程的上下文中使用他时给出编译时报错。我们将在第 16 章中讨论怎样在多线程的程序中获得 `RefCell` 的功能。 -与 `Rc` 类似,`RefCell` 只适用于单线程场景,如果咱们试图在多线程环境下使用它,会出现编译时错误。我们将在第 16 章讨论如何在多线程程序中获得 `RefCell` 的功能。 +下面是对选择 `Box`、`Rc` 或 `RefCell` 的原因的回顾: -下面是对选择 `Box`、`Rc` 或 `RefCell` 理由的总结: ++ 所有者数量方面 + - `Rc` 允许同一数据可以有多个所有者; + - `Box` 和 `RefCell` 都只有单一的所有者。 ++ 借用规则检查方面 + - `Box` 允许不可变或可变的借用在编译时检查; + - `Rc` 只允许不可变借用在编译时检查; + - `RefCell` 允许不可变或可变的借用在运行时检查。 +- 由于 `RefCell` 允许可变借用在运行时检查,因此即使 `RefCell` 是不可变的,咱们也可以改变 `RefCell` 内部的值。 -- `Rc` 使同一数据有多个所有者;`Box` 和 `RefCell` 有单一所有者; -- `Box` 允许在编译时检查不可变或可变的借用;`Rc` 只允许在编译时检查不可变的借用;`RefCell` 允许在运行时检查不可变或可变的借用; -- 因为 `RefCell` 允许在运行时检查可变的借用,所以即使 `RefCell` 是不可变的,咱们也可以改变 `RefCell` 中的值。 - -改变不可变值内部的值,就是 *内部可变性模式,the interior mutablity pattern*。让我们看一下内部可变性有用的一种情况,并检视其如何可行。 +改变不可变值内部的值,即为 *内部可变性模式*。我们来看一种其下内部可变性有用的情形,并研究这是如何可行的。 -## 内部可变性:对不可变值的可变借用 +## 使用内部可变性 -**Interior Mutability: A Mutable Borrow to an Immutable Value** - - -借用规则的一种后果是,当咱们有一个不可变的值时,咱们不能以可变方式借用他。比如,下面这段代码就不能编译: +借用规则的一个结果是,当咱们有个不可变值时,咱们就无法以可变方式借用他。例如,下面这段代码不会编译: ```rust fn main() { @@ -53,55 +49,103 @@ fn main() { } ``` -如果咱们试图编译这段代码,咱们会得到以下错误: +当咱们尝试编译这段代码时,将得到以下报错: ```console $ cargo run - Compiling sp_demos v0.1.0 (/home/lennyp/rust-lang/sp_demos) + Compiling borrowing v0.1.0 (/home/hector/rust-lang-zh_CN/projects/borrowing) error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable --> src/main.rs:3:13 | -2 | let x = 5; - | - help: consider changing this to be mutable: `mut x` 3 | let y = &mut x; | ^^^^^^ cannot borrow as mutable + | +help: consider changing this to be mutable + | +2 | let mut x = 5; + | +++ For more information about this error, try `rustc --explain E0596`. -error: could not compile `sp_demos` due to previous error; +error: could not compile `borrowing` (bin "borrowing") due to 1 previous error ``` -然而,在有些情况下,值在其方法中改变自身,但对其他代码来说却显得不可改变,这将是非常有用的。在该值的方法之外的代码将不能改变该值。使用 `RefCell` 是获得内部可变性能力的一种方法,但是 `RefCell` 并没有完全绕过借用规则:编译器中的借用检查器会放行这种内部可变性,而代之以在运行时借用规则得以检查。如果咱们违反了这些规则,咱们会得到一个 `pani!` 而不是一个编译器报错。 - -咱们来通过一个其中咱们可以使用 `RefCell` 改变一个不可变的值的实际示例,看看为什么这很有用。 -## 内部可变性的用例:模拟对象 +然而,在某些情况下,值在其方法中改变自身,却对其他代码表现为不可变,会非常有用。值的方法外部的代码将无法修改该值。使用 `RefCell` 是获得具备内部可变性能力的一种方式,但 `RefCell` 并未完全绕过借用规则:编译器中的借用检查器会放行这种内部可变性,而代之以在运行时检查借用规则。当咱们违反这些规则时,咱们将得到一次 `panic!` 而不是编译器报错。 -**A Use Case for Interior Mutability: Mock Objects** +咱们来通过一个实际示例,其中我们可以使用 `RefCell` 来改变某个不可变值,看看为什么这很有用。 -有时在测试过程中,程序员会使用一个类型来代替另一类型,以便观察特定的行为并断定其实现是正确的。这种占位符类型被称为 *测试替身,test double*。请从电影制作中的“特技替身,stunt double”的角度来考虑他,某人代替一名演员来完成特别棘手的一场戏。当咱们运行测试时,测试替身代表其他类型。*模拟对象,mock objects* 是特定类型的测试替身,他记录了测试过程中发生的事情,因此咱们可以断言发生了正确的动作。 +## 以模拟对象测试 -Rust 没有像其他语言那样拥有对象,Rust 也没有像其他一些语言那样在标准库中内置模拟对象功能。但是,咱们绝对可以创建一个与模拟对象具有相同用途的结构。 +在测试过程中,为了观察特定行为并断言其是否得以正确实现,程序员有时会使用一种类型代替另一类型。这种占位类型称为 *测试替身*。可以把他想象成电影制作中的特技替身,有一个人顶替演员,完成特别棘手的某场戏。当我们运行测试时,测试替身会代替其他类型。所谓 *模拟对象*,属于测试替身的具体类型,他会记录测试过程中发生的情况,以便咱们可以断言是否发生了正确的操作。 -下面是我们要测试的情景:我们将创建一个库,跟踪某个数值与最大值的关系,并根据当前数值与最大值的接近程度发送消息。例如,这个库可以用来跟踪用户允许调用的 API 数量配额。 +Rust 没有其他语言有的意义上的对象,并且 Rust 没有一些其他语言有的内建于标准库中的模拟对象功能。但是,咱们完全可以创建一个结构体,将发挥与模拟对象同样的作用。 -这个库将提供跟踪某个值接近最大值的程度,及在什么时刻发出什么消息的功能。使用这个库的应用,将被期望提供发送消息的机制:应用可以在应用中放置消息、发送电子邮件、发出手机短信或其他东西。库不需要知道这个细节。他所需的只是实现了咱们将提供的名为 `Messenger` 特质的东西。以下清单 15-20 给出库的代码: +以下是我们将测试的情景:我们将创建一个库,跟踪某个值与最大值的关系,并根据当前值与最大值的接近程度发送消息。例如,这个库可用于跟踪用户允许进行的 API 调用数量的配额。 +我们的库将仅提供跟踪某个值接近最大值的程度,以及于何时发出什么消息的功能。使用我们的库的应用,需自行提供发送消息的机制:应用可以直接展示消息给用户、发送电子邮件、发送短信或执行其他操作。库无需了解这些细节。他所需的只是实现我们将提供的一个特质,名为 `Messenger`。以下清单 15-20 展示了该库的代码: + 文件名:`src/lib.rs` ```rust -{{#include ../../projects/limit_tracker/src/lib.rs::35}} +pub trait Messenger { + fn send(&self, msg: &str); +} + +pub struct LimitTracker<'a, T: Messenger> { + messenger: &'a T, + value: usize, + max: usize, +} + +impl<'a, T> LimitTracker<'a, T> +where + T: Messenger, +{ + pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, 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 >= 1.0 { + self.messenger.send("Error: 你已超出配额!"); + } else if percentage_of_max >= 0.9 { + self.messenger + .send("Urgent warning: 你已用完 90% 的配额!"); + } else if percentage_of_max >= 0.75 { + self.messenger + .send("Warning: 你已用完 75% 的配额!"); + } + } +} ``` -*清单 15-20:跟踪某个值与最大值接近程度,并在值处于不同水平时发出告警的库* +**清单 15-20**:跟踪某个值与最大值接近程度,并在值处于特定水平时告警的一个库 -这段代码的一个重要部分是 `Messenger` 特质有个叫做 `send` 的方法,其接收一个不可变 `self` 的引用和消息文本。这个特质是咱们模拟对象需要实现的接口,这样模拟对象就可以和真实对象一样被使用。另一个重要的部分是,我们要测试 `LimitTracker` 上 `set_value` 方法的行为。我们可以改变我们传入的 `value` 参数,但 `set_value` 并没有返回任何东西让我们做断言。我们希望能够表达出,若咱们用实现了 `Messenger` 特质的东西,与 `max` 的一个特定值创建了一个 `LimitTracker`,当我们为 `value` 传递不同的数字时,`messenger` 就会被告知要发送相应的消息。 +这段代码的一个重要部分在于,`Messenger` 特质有个名为 `send` 的方法,他会取对 `self` 的不可变引用,以及消息文本。这个特质是我们的模拟对象需要实现的接口,以便模拟对象可以像真实对象一样使用。另一个重要的部分是,我们希望测试 `LimitTracker` 上的 `set_value` 方法的行为。我们可以改变我们传入的 `value` 参数的内容,但 `set_value` 不会返回任何内容供我们进行断言。我们希望能够表达,当咱们以实现 `Messenger` 特质的项目与特定的 `max` 值创建一个 `LimitTracker` 时,消息发送器会在我们为 `value` 传递不同数字时,被告知要发送相应的消息。 -> 注:the interface, 借鉴了 Java 语言的叫法,参见 [使用接口来拯救!](https://java.xfoss.com/ji-cheng-he-duo-tai-ji-zhi/ch08_interfaces_and_abstract_classes#interface_rescue)。而这种内部可变性模式用到的数据结构,则类似于 Java 中的内部类。 +> **译注**:the interface, 借鉴了 Java 语言的叫法,参见 [使用接口来拯救!](https://java.xfoss.com/Ch08_Interfaces_and_Abstract_Classes.html#%E4%BD%BF%E7%94%A8%E6%8E%A5%E5%8F%A3%E6%9D%A5%E6%8B%AF%E6%95%91)。而这种内部可变性模式用到的数据结构,则类似于 Java 中的内部类。 -我们需要一个模拟对象,他不会在我们调用 `send` 时发送电子邮件或文本消息,而只会记录他被告知要发送的消息。我们可以创建模拟对象的一个新实例,创建一个使用该模拟对象的 `LimitTracker` 实例,调用 `LimitTracker` 实例的 `set_value` 方法,然后检查该模拟对象是否有我们期望的消息。清单 15-21 给出了一个实现模拟对象的尝试,来就这样做,但借用检查器不允许这样做: +我们需要这样一个模拟对象,当我们调用 `send` 时,他不会发送电子邮件或短信,而只将跟踪他被告知要发送的消息。我们可以 +- 创建一个这种模拟对象的新实例、 +- 创建一个使用该模拟对象的 `LimitTracker` 实例、 +- 调用 `LimitTracker` 上的 `set_value` 方法, +- 然后检查该模拟对象是否具有我们期望的消息。 + + +下面清单 15-21 展示了一种实现一个模拟对象来做到这点的尝试,但借用检查器不允许这样做。 + + 文件名:`src/lib.rs` ```rust @@ -139,63 +183,101 @@ mod tests { } ``` -*清单 15-21:试图实现一个借用检查器不允许的 `MockMessenger`* +**清单 15-21**:尝试实现借用检查器不允许的 `MockMessenger` -这段测试代码定义了一个 `MockMessenger` 结构体,他有一个 `send_messages` 字段,里面有一个 `String` 值的 `Vec`,用来记录他被告知要发送的消息。我们还定义了一个关联函数 `new`,以方便创建新的 `MockMessenger` 值,该值以一个空的消息列表开始。然后我们为 `MockMessenger` 实现了 `Messenger` 特质,这样我们就可以给 `LimitTracker` 一个 `MockMessenger`。在 `send` 方法的定义中,我们将传入的消息作为参数,并将其存储在 `MockMessenger` 的 `send_messages` 列表中。 +这段测试代码定义了一个 `MockMessenger` 结构体,有着一个 `sent_messages` 字段,该字段带有一个 `String` 值的 `Vec` 值,用于跟踪其被告知要发送的消息。我们还定义了个关联函数 `new`,以便于创建以空消息列表开头的新 `MockMessenger` 值。然后,我们为 `MockMessenger` 实现 `Messenger` 特质,以便我们可以提供 `MockMessenger` 给 `LimitTracker`。在 `send` 方法的定义中,我们取作为参数传入的消息,并存储在 `MockMessenger` 的 `sent_messages` 列表中。 -在测试中,我们正在测试当 `LimitTracker` 被告知将 `value` 设置为超过最大值的 75% 时会发生什么。首先,我们创建一个新的 `MockMessenger`,他将以一个空的消息列表开始。然后我们创建一个新的 `LimitTracker` 并为其提供对新 `MockMessenger` 的引用和最大值 `100`。我们在 `LimitTracker` 上用一个大于 75% 的值 `80` 调用 `set_value` 方法。然后我们断言 `MockMessenger` 正在跟踪的消息列表现在应有一条消息。 +在测试中,我们正在测试当 `LimitTracker` 被告知设置 `value` 为大于最大值的 75% 的某个值时会发生什么。首先,我们创建一个新的 `MockMessenger`,他将以一个空的消息列表开始。然后,我们创建一个新的 `LimitTracker` 并给予他一个对新的 `MockMessenger` 的引用和 `100` 的 `max` 值。我们以值 `80`,其大于 75%,调用 `LimitTracker` 上 `set_value` 方法。然后,我们断言 `MockMessenger` 跟踪的消息列表中现在应该有一条消息。 -但是,此测试存在一个问题,如下所示: +但是,如下所示,这个测试存在一个问题: ```console $ cargo test - Compiling limit_tracker v0.1.0 (/home/lennyp/rust-lang/limit_tracker) + Compiling limit-tracker v0.1.0 (/home/hector/rust-lang-zh_CN/projects/limit-tracker) error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference - --> src/lib.rs:58:13 + --> src/lib.rs:59:13 | -2 | fn send(&self, msg: &str); - | ----- help: consider changing that to be a mutable reference: `&mut self` +59 | self.sent_messages.push(String::from(message)); + | ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so it cannot be borrowed as mutable + | +help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition + | + 2 ~ fn send(&mut self, msg: &str); + 3 | } ... -58 | self.sent_messages.push(String::from(message)); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable +57 | impl Messenger for MockMessenger { +58 ~ fn send(&mut self, message: &str) { + | For more information about this error, try `rustc --explain E0596`. -error: could not compile `limit_tracker` due to previous error -warning: build failed, waiting for other jobs to finish... +error: could not compile `limit-tracker` (lib test) due to 1 previous error ``` -我们不能修改 `MockMessenger` 来记录消息,因为 `send` 方法需要一个对 `self` 的不可变的引用。我们也不能采纳错误文本中的建议,使用 `&mut self` 来代替,因为那样的话,`send` 的签名就无法与 `Messenger` 特质定义中的签名相匹配(请随意尝试,看看咱们会得到什么样的报错消息)。 +我们无法修改 `MockMessenger` 来记录消息,因为 `send` 方法取对 `self` 的不可变引用。我们也无法采纳错误文本中的建议,在 `impl` 方法和特质定义中都使用 `&mut self`。我们不希望仅仅为了测试而修改 `Messenger` 特质。我们需要找到一种方法,让测试代码在现有设计下正常运行。 -这种情况下,内部可变性可以起到帮助作用!我们将把 `send_messages` 存储在一个 `RefCell` 中,然后 `send` 方法将能够修改 `send_messages` 来存储我们所看到的信息。清单 15-22 显示了这是什么样子: +这正是内部可变性可以发挥作用的情形!我们将存储 `sent_messages` 于 `RefCell` 中,然后 `send` 方法就能够修改 `sent_messages` 以存储我们看到的消息。下面清单 15-22 展示了他的样子: + 文件名:`src/lib.rs` ```rust -{{#include ../../projects/limit_tracker/src/lib.rs:37:}} +#[cfg(test)] +mod tests { + use super::*; + use std::cell::RefCell; + + struct MockMessenger { + sent_messages: RefCell>, + } + + 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() { + // -- 跳过代码 -- + + assert_eq!(mock_messenger.sent_messages.borrow().len(), 1); + } +} ``` -*清单 15-22:使用 `RefCell` 来改变内层值,而外部值被认为是不可变的* +**清单 15-22**:使用 `RefCell` 在外部值被视为不可变的同时修改内层值 -`sent_messages` 字段现在的类型是 `RefCell`,而不是 `Vec`。在 `new` 函数中,我们围绕空向量创建一个新的 `RefCell` 实例。 +`sent_messages` 字段现在的类型为 `RefCell`,而不是 `Vec`。在 `new` 函数中,我们围绕空矢量值创建了个新的 `RefCell` 实例。 -对于 `send` 方法的实现,第一个参数仍然是 `self` 的不可变借用,这与特质定义相匹配。我们对 `self.send_messages` 中的 `RefCell` 调用 `borrow_mut`,以获得 `RefCell` 中值的可变引用,也就是那个矢量。然后,我们可以对该矢量的可变引用调用 `push`,以记录测试期间发送的消息。 +对于 `send` 方法的实现,第一个参数仍然是 `self` 的不可变借用,这与特质的定义一致。我们对 `self.sent_messages` 中的 `RefCell` 调用了 `borrow_mut`,以获取对 `RefCell` 内部的值,即那个矢量值的可变引用。然后,我们就可以调用到该矢量值的可变引用上的 `push`,来记录测试过程中发送的消息。 -我们必须做的最后一个更改是在断言中:为了查看内层矢量中有多少个条目,我们在 `RefCell>` 上调用 `borrow` 以获得对该矢量的不可变引用。 +我们必须要做的最后一个更改是在断言中:为了查看内层矢量值中有多少个项目,我们对 `RefCell>` 调用 `borrow`,以获取对该矢量值的不可变引用。 -现在咱们已经看到了如何使用 `RefCell`,咱们来深入了解其工作原理! +现在咱们已经了解了怎样使用 `RefCell`,我们来深入了解他的工作原理! -## 使用 `RefCell` 在运行时记录借用 +## 在运行时跟踪借用 -**Keeping Track of Borrows at Runtime with `RefCell`** +在创建不可变与可变的引用时,我们分别使用 `&` 和 `&mut` 语法。而对于 `RefCell`,我们使用 `borrow` 和 `borrow_mut` 方法,他们是属于 `RefCell` 的安全 API 的一部分。 +- `borrow` 方法返回灵巧指针类型 `Ref`, +- 而 `borrow_mut` 返回灵巧指针类型 `RefMut`。 -当创建不可变和可变引用时,我们分别使用 `&` 和 `&mut` 语法。而对于 `RefCell`,我们使用 `borrow` 和 `borrow_mut` 方法,他们属于 `RefCell` 安全 API 的一部分。`borrow` 方法返回灵巧指针类型 `Ref`,而 `borrow_mut` 返回灵巧指针类型 `RefMut`。这两种类型都实现了 `Deref`,所以我们可以像对待普通引用一样对待他们。 +这两种类型都实现了 `Deref`,因此我们可以像对待普通引用一样对待他们。 -`RefCell` 会记录当前有多少个 `Ref` 和 `RefMut` 灵巧指针是活动的。每次我们调用 `borrow`,`RefCell` 都会增加他的计数,即有多少个不可变借用是活动的。当一个 `Ref` 值超出作用域时,不可变借用的计数就会减少一个。就像编译时的借用规则一样,`RefCell` 允许我们在任何时候有许多不可变借用或一个可变的借用。 +`RefCell` 会跟踪当前有多少个活动的 `Ref` 和 `RefMut` 灵巧指针。每次我们调用 `borrow` 时,`RefCell` 都会增加其活动的不可变借用的计数。当某个 `Ref` 值超出作用域时,不可变借用计数会减少 1。就像编译时的借用规则一样,`RefCell` 允许我们在任何时候都可以有多个不可变借用,或者一个可变借用。 -在咱们尝试违反这些规则时,与在引用下咱们会得到编译器报错不同,`RefCell` 的实现将在运行时终止运行。下面清单 15-23 给出了清单 15-22 中那个 `send` 实现的修改。咱们故意为同一作用域创建了两个可变借用,以演示 `RefCell` 在运行时阻止咱们这样做。 +当我们试图违反这些规则时,与我们在引用下将得到编译器报错不同,`RefCell` 的实现将在运行时终止运行。下面清单 15-23 展示了清单 15-22 中的 `send` 实现的一种修改。我们故意尝试为同一作用域创建两个可变借用,以说明 `RefCell` 会阻止咱们在运行时这样做。 + 文件名:`src/lib.rs` ```rust @@ -204,81 +286,104 @@ warning: build failed, waiting for other jobs to finish... let mut borrow_one = self.sent_messages.borrow_mut(); let mut borrow_two = self.sent_messages.borrow_mut(); - borrow_one.push(String::from(message)); - borrow_two.push(String::from(message)); + one_borrow.push(String::from(message)); + two_borrow.push(String::from(message)); } } ``` -*清单 15-23:在同一作用域中创建两个可变引用,以发现 `RefCell` 会终止运行* +**清单 15-23**:在同一作用域中创建两个可变引用,以验证 `RefCell` 将终止运行 -我们为从 `borrow_mut` 返回的 `RefMut` 灵巧指针创建了一个变量 `one_borrow`。然后我们以同样的方式在变量 `two_borrow` 中创建了另一个可变的借用。这就在同一作用域中产生了两个可变引用,这是不允许的。当我们运行咱们库的测试时,清单 15-23 中的代码将被不带任何报错地编译,但测试将失败: +我们为从 `borrow_mut` 返回的 `RefMut` 灵巧指针创建了个变量 `one_borrow`。然后,我们以同样的方式在变量 `two_borrow` 中创建了另一个可变借用。这会在同一作用域中构造两个可变引用,而这是不被允许的。当我们运行库的测试时,清单 15-23 中的代码将不带任何报错地编译,但测试将失败: ```console $ cargo test - Compiling limit_tracker v0.1.0 (/home/peng/rust-lang/limit_tracker) - Finished test [unoptimized + debuginfo] target(s) in 0.46s - Running unittests src/lib.rs (target/debug/deps/limit_tracker-98d6159d1b15eb72) + Compiling limit-tracker v0.1.0 (/home/hector/rust-lang-zh_CN/projects/limit-tracker) + Finished `test` profile [unoptimized + debuginfo] target(s) in 0.09s + Running unittests src/lib.rs (target/debug/deps/limit_tracker-97fd84dd2716b118) running 1 test -test tests::it_sends_an_over_75_percent_waring_message ... FAILED +test tests::it_sends_an_over_75_percent_warning_message ... FAILED failures: ----- tests::it_sends_an_over_75_percent_waring_message stdout ---- -thread 'tests::it_sends_an_over_75_percent_waring_message' panicked at 'already borrowed: BorrowMutError', src/lib.rs:60:53 +---- tests::it_sends_an_over_75_percent_warning_message stdout ---- + +thread 'tests::it_sends_an_over_75_percent_warning_message' (678390) panicked at src/lib.rs:61:53: +RefCell already borrowed note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: - tests::it_sends_an_over_75_percent_waring_message + tests::it_sends_an_over_75_percent_warning_message test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` ``` -请注意该代码是以消息 `already borrowed: BorrowMutError` 终止运行的。这正是 `RefCell` 处理运行时违反借用规则的方式。 +请注意,该代码以消息 `RefCell already borrowed` 终止运行。这正是 `RefCell` 处理运行时违反借用规则的方式。 -选择在运行时而不是编译时捕获借用错误,正如我们在这里所做的那样,意味着咱们可能会在开发过程后期,才发现代码中的错误:可能直到咱们的代码部署到生产环境中才发现。此外,由于在运行时而不是编译时记录借用,咱们的代码会招致小的运行时性能损失。但是,使用 `RefCell` 可以编写出模拟对象,该对象可以修改自身,来记录在咱们仅允许不可变值的上下文中使用他时,其所见到的消息。尽管 `RefCell` 为获得比常规引用所提供的更多功能而有所取舍,咱们可以使用他。 +像我们在这里这样,选择在运行时而非编译时捕获借用错误,意味着咱们可能会在开发过程后期,才发现代码中的错误:甚至可能直到代码部署到生产环境中才会发现。此外,由于在运行时而不是编译时跟踪借用,咱们的代码会因此出现较小的运行时性能损失。但是,使用 `RefCell` 使编写模拟对象可行,其可以修改本身以跟踪他看到的消息,而咱们是在只允许不可变值的上下文中使用他。尽管需要权衡,咱们仍可使用 `RefCell` 来获得比普通引用提供的更多功能。 -## 通过结合 `Rc` 与 `RefCell`,实现可变数据的多个所有者 +## 允许可变数据的多个所有者 -**Having Multiple Owners of Mutable Data by Combining `Rc` and `RefCell`** +使用 `RefCell` 的一种常见方式,是与 `Rc` 结合使用。回顾一下,`Rc` 允许咱们有着某一数据的多个所有者,但他只提供对数据的不可变访问。当咱们有个包含 `RefCell` 的 `Rc` 时,就可以得到一个可以有多个所有者,*并且* 咱们可以改变的值! +例如,回顾清单 15-18 中的构造列表示例,其中我们使用了 `Rc` 来允许多个列表共用另一列表的所有权。由于 `Rc` 仅包含不可变值,因此一旦创建了列表后,我们就无法更改列表中的任何值。我们来加入 `RefCell`,以获得其修改列表中值的能力。下面清单 15-24 显示了通过在 `Cons` 定义中使用 `RefCell`,我们可以修改存储在所有列表中的值: -使用 `RefCell` 的一种常见方式是与 `Rc` 结合使用。回顾一下,`Rc` 实现了某个数据的多个所有者,但只提供对数据的不可变访问。如果咱们有一个持有 `RefCell` 的 `Rc`,咱们可以得到一个可以有着多个所有者,*且* 咱们可以改变的值。 - -比如,回顾清单 15-18 中的构造列表示例,咱们使用 `Rc` 来实现多个列表共用另一列表所有权。由于 `Rc` 只保存不可变值,因此一旦咱们创建出列表中的任何值,咱们就再也不能改变他们。咱们来加入 `RefCell`,以获得修改列表中值的能力。下面清单 15-24 显示,通过在 `Cons` 定义中使用 `RefCell`,咱们可以修改所有列表中存储的值: - + 文件名:`src/main.rs` ```rust -{{#include ../../projects/cons_list_demo/src/main.rs}} +#[derive(Debug)] +enum List { + Cons(Rc>, Rc), + Nil, +} + +use crate::List::{Cons, Nil}; +use std::cell::RefCell; +use std::rc::Rc; + +fn main() { + let value = Rc::new(RefCell::new(5)); + + let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil))); + + let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a)); + let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a)); + + *value.borrow_mut() += 10; + + println!("a 随后 = {a:?}"); + println!("b 随后 = {b:?}"); + println!("c 随后 = {c:?}"); +} ``` -*清单 15-24:使用 `Rc>` 创建一个咱们可改变的 `List`* +**清单 15-24**:使用 `Rc>` 创建我们可以改变的 `List` -我们创建了一个值,他是 `Rc>` 的一个实例,并将其存储在一个名为 `value` 的变量中,以便我们稍后可以直接访问。然后我们以持有 `value` 的一个 `Cons` 变种,在 `a` 中创建了一个 `List`。我们需要克隆 `value`,以便 `a` 和 `value` 都拥有内部值 `5` 的所有权,而不是将所有权从 `value` 转移到 `a` 或让 `a` 从 `value` 借用。 +我们创建一个值 `Rc>` 的实例的值,并存储在名为 `value` 的变量中,以便稍后可以直接访问他。然后,我们以包含 `value` 的一个 `Cons` 变种创建一个 `a` 中的 `List`。我们需要克隆 `value`,以便 `a` 和 `value` 都拥有内部的值 `t` 的所有权,而不是从 `value` 转移所有权到 `a`,或者让 `a` 从 `value` 借用。 -我们将列表 `a` 包装在 `Rc` 中,这样当我们创建列表 `b` 和 `c` 时,他们都可以引用 `a`,这就是我们在示例 15-18 中所做的。 +我们将列表 `a` 包装在 `Rc` 中,这样当我们创建列表 `b` 和 `c` 时,他们都可以引用 `a`,这就是我们在 [清单 15-18](./rc-t.md#listing_15-18) 中所做的。 -在我们创建了 `a`、`b` 和 `c` 中的列表后,我们打算在 `value` 中的值上加 `10`。我们通过在 `value` 上调用 `borrow_mut` 来实现这一目的,他使用了我们在第 `5` 章中讨论过的自动解引用功能,the automatic dereferencing feature,(参见 [`->` 操作符去哪儿了?](Ch05_Using_Structs_to_Structure_Related_Data.md#--操作符the---operator哪去了呢) 小节),将 `Rc` 解引用到内部的 `RefCell` 值。`borrow_mut` 方法返回一个 `RefMut` 灵巧指针,我们对其使用解引用操作符,并改变内部值。 +在创建了 `a`、`b` 和 `c` 中的列表后,我们打算加 10 到 `value` 中的值。我们通过对 `value` 调用 `borrow_mut` 来做到这点,他使用我们在第 5 章中 [`->` 操作符在哪里?](../structs/method_syntax.md#auto_deref) 处讨论的自动解引用特性,解引用 `Rc` 为内层的 `RefCell` 值。`borrow_mut` 方法返回一个 `RefMut` 灵巧指针,我们对其使用解引用运算符并修改内层值。 -在打印 `a`、`b` 与 `c` 时,就可以看到他们都有了修改后的值 `15` 而非 `5`: +当我们打印 `a`、`b` 与 `c` 时,可以看到他们都有了修改后的值 `15` 而不是 `5`: ```console -$ cargo run lennyp@vm-manjaro - Compiling cons_list_demo v0.1.0 (/home/lennyp/rust-lang/cons_list_demo) - Finished dev [unoptimized + debuginfo] target(s) in 0.17s - Running `target/debug/cons_list_demo` -之后的 a = Cons(RefCell { value: 15 }, Nil) -之后的 b = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil)) -之后的 c = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil)) +$ cargo run + Compiling cons-list v0.1.0 (/home/hector/rust-lang-zh_CN/projects/cons-list) + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s + Running `target/debug/cons-list` +a 随后 = Cons(RefCell { value: 15 }, Nil) +b 随后 = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil)) +c 随后 = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil)) ``` -这个技巧非常整洁! 通过使用 `RefCell`,我们有一个对外不可变的 `List` 值。但是我们可以使用 `RefCell` 上提供对其内部可变性访问的方法,这样我们就可以在需要的时候修改我们的数据。借用规则的运行时检查可以保护我们不受数据竞赛的影响,有时值得用一点速度来换取我们数据结构中的这种灵活性。请注意,`RefCell` 对多线程代码不起作用! `Mutex` 是 `RefCell` 的线程安全版本,我们将在第 16 章讨论 `Mutex`。 +这种技巧非常巧妙! 通过使用 `RefCell`,我们有了个表面上不可变的 `List` 值。但我们可以使用 `RefCell` 上,提供对其内部可变性的访问的方法,以便可以在需要时修改数据。运行时对借用规则的检查,可以保护我们免受数据竞争的影响,有时以牺牲一点速度,换取这种数据结构方面的灵活性是值得的。请注意,`RefCell` 不适用于多线程代码! `Mutex` 是 `RefCell` 的线程安全版本,我们将在第 16 章中讨论 `Mutex`。 (End) diff --git a/src/structs/method_syntax.md b/src/structs/method_syntax.md index 26fddbe..df235cc 100644 --- a/src/structs/method_syntax.md +++ b/src/structs/method_syntax.md @@ -83,6 +83,7 @@ fn main() { 通常,但并非总是,当我们给方法取了与字段同样的名字时,我们就会希望他只返回该字段中的值,而不做其他任何事情。像这样的方法称为 *获取器,getter*,而 Rust 并未像一些别的语言那样自动对结构字段实现他们。获取器很有用,因为我们可以将字段构造为私有,而将方法构造为公开,从而作为类型公开 API 的一部分,实现对字段的只读访问。我们将在 [第 7 章](../packages_crates_and_modules/paths.md#使用-pub-关键字对路径进行暴露) 中讨论什么是公开和私有,以及如何将字段或方法指定为公开或私有。 + > **`->` 操作符在哪里?** > > 在 C 和 C++ 中,有两种不同的运算符用于调用方法:当咱们直接在对象上调用方法时,咱们会使用 `.`;而当咱们在指向某个对象的指针上调用方法,且需要首先解引用该指针时,咱们就要使用 `->`。换句话说,当 `object` 是个指针时,那么 `object->something()` 就类似于 `(*object).something()`。