From 79d364c82420c91a56c5fa537b816f96b982af63 Mon Sep 17 00:00:00 2001 From: mg-chao Date: Tue, 4 Jan 2022 22:09:01 +0800 Subject: [PATCH 1/5] perf: expression --- course-book/contents/advance/lifetime/advance.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/course-book/contents/advance/lifetime/advance.md b/course-book/contents/advance/lifetime/advance.md index 8e232495..75c4a1fe 100644 --- a/course-book/contents/advance/lifetime/advance.md +++ b/course-book/contents/advance/lifetime/advance.md @@ -94,7 +94,8 @@ fn main() { ``` -该段代码不能通过编译的原因,是因为某个借用不再需要,但编译器不理解这点,反而谨慎的给该借用安排了一个很大的作用域,结果导致后续的借用失败: +该段代码不能通过编译的原因是编译器未能精确地判断出某个可变借用不再需要,反而谨慎的给该借用安排了一个很大的作用域,结果导致后续的借用失败: + ```console error[E0499]: cannot borrow `*map` as mutable more than once at a time --> src/main.rs:13:17 @@ -116,14 +117,14 @@ error[E0499]: cannot borrow `*map` as mutable more than once at a time | |_________- returning this value requires that `*map` is borrowed for `'m` ``` -可以看出,在`match map.get_mut(&key)`方法调用完成后,对`map`的可变借用就结束了,但是由于编译器不太聪明,它认为该借用会持续到整个`match`语句块的结束(第16行处),结果导致了后续借用的失败。 +分析代码可知在`match map.get_mut(&key)`方法调用完成后,对`map`的可变借用就可以结束了。但从报错看来,编译器不太聪明,它认为该借用会持续到整个`match`语句块的结束(第16行处),这便造成了后续借用的失败。 类似的例子还有很多,由于篇幅有限,就不在这里一一列举,如果大家想要阅读更多的类似代码,可以看看[<>](https://github.com/sunface/rust-codes)一书。 ## 无界生命周期 -不安全代码(`unsafe`)经常会凭空产生引用或生命周期, 这些生命周期被称为是**无界(unbound)**的。 +不安全代码(`unsafe`)经常会凭空产生引用或生命周期, 这些生命周期被称为是 **无界(unbound)** 的。 无界生命周期往往是在解引用一个原生指针(裸指针raw pointer)时产生的,换句话说,它是凭空产生的,因为输入参数根本就没有这个生命周期: ```rust @@ -226,7 +227,7 @@ let closure_slision = |x: &i32| -> &i32 { x }; 编译器就必须深入到闭包函数体中,去分析和推测生命周期,复杂度因此极具提升:试想一下,编译器该如何从复杂的上下文中分析出参数引用的生命周期和闭包体中生命周期的关系? -由于上述原因(当然,实际情况复杂的多),Rust语言开发者其实目前是有意为之,针对函数和闭包实现了两种不同的生命周期消除规则。 +由于上述原因(当然,实际情况复杂的多),Rust语言开发者目前其实是有意针对函数和闭包实现了两种不同的生命周期消除规则。 ## NLL(Non-Lexical Lifetime) @@ -314,7 +315,7 @@ struct Ref<'a, T> { } ``` -在本节的生命周期约束中,也提到过,新版本Rust中,上面情况中的`T: 'a`可以被消除掉,当然,你也可以显式的声明,但是会影响代码可读性。关于类似的场景,Rust团队计划在未来提供更多的消除规则,但是,你懂得,计划未来就等于未知。 +在本节的生命周期约束中,也提到过,新版本Rust中,上面情况中的`T: 'a`可以被消除掉,当然,你也可以显式的声明,但是会影响代码可读性。关于类似的场景,Rust团队计划在未来提供更多的消除规则,但是,你懂的,计划未来就等于未知。 ## 一个复杂的例子 下面是一个关于生命周期声明过大的例子,会较为复杂,希望大家能细细阅读,它能帮你对生命周期的理解更加深入。 @@ -357,8 +358,8 @@ fn main() { println!("Interface should be dropped here and the borrow released"); - // this fails because inmutable/mutable borrow - // but Interface should be already dropped here and the borrow released + // 下面的调用会失败,因为同时有不可变/可变借用 + // 但是Interface在之前调用完成后就应该被释放了 use_list(&list); } From 3729826dc483e9e14dec8ba6ee937ce8ba935ad7 Mon Sep 17 00:00:00 2001 From: mg-chao Date: Tue, 4 Jan 2022 22:12:28 +0800 Subject: [PATCH 2/5] perf --- course-book/contents/advance/lifetime/advance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/course-book/contents/advance/lifetime/advance.md b/course-book/contents/advance/lifetime/advance.md index 75c4a1fe..1dc1e1ae 100644 --- a/course-book/contents/advance/lifetime/advance.md +++ b/course-book/contents/advance/lifetime/advance.md @@ -94,7 +94,7 @@ fn main() { ``` -该段代码不能通过编译的原因是编译器未能精确地判断出某个可变借用不再需要,反而谨慎的给该借用安排了一个很大的作用域,结果导致后续的借用失败: +这段代码不能通过编译的原因是编译器未能精确地判断出某个可变借用不再需要,反而谨慎的给该借用安排了一个很大的作用域,结果导致后续的借用失败: ```console error[E0499]: cannot borrow `*map` as mutable more than once at a time From 9064b9fd4089df37ceb3921644f80ebf40a23d23 Mon Sep 17 00:00:00 2001 From: sunface Date: Wed, 5 Jan 2022 11:03:21 +0800 Subject: [PATCH 3/5] add Rc/Arc --- course-book/contents/SUMMARY.md | 6 +- .../advance/smart-pointer/cell-refcell.md | 4 + .../contents/advance/smart-pointer/cell.md | 4 - .../contents/advance/smart-pointer/rc-arc.md | 201 ++++++++++++++++++ .../advance/smart-pointer/rc-refcell.md | 1 - course-book/contents/compiler/speed-up.md | 6 +- course-book/contents/performance/cpu-cache.md | 6 +- course-book/contents/performance/intro.md | 5 +- .../contents/performance/runtime-check.md | 5 +- .../contents/pitfalls/iterator-everywhere.md | 107 ++++++++++ course-book/contents/practice/best-pratice.md | 5 +- course-book/writing-material/posts/atomic.md | 1 + course-book/writing-material/posts/threads.md | 5 +- 13 files changed, 342 insertions(+), 14 deletions(-) create mode 100644 course-book/contents/advance/smart-pointer/cell-refcell.md delete mode 100644 course-book/contents/advance/smart-pointer/cell.md create mode 100644 course-book/contents/advance/smart-pointer/rc-arc.md delete mode 100644 course-book/contents/advance/smart-pointer/rc-refcell.md create mode 100644 course-book/contents/pitfalls/iterator-everywhere.md create mode 100644 course-book/writing-material/posts/atomic.md diff --git a/course-book/contents/SUMMARY.md b/course-book/contents/SUMMARY.md index fa65480d..029c8e84 100644 --- a/course-book/contents/SUMMARY.md +++ b/course-book/contents/SUMMARY.md @@ -68,8 +68,8 @@ - [Box堆对象分配](advance/smart-pointer/box.md) - [Deref解引用](advance/smart-pointer/deref.md) - [Drop释放资源](advance/smart-pointer/drop.md) - - [Cell与RefCell todo](advance/smart-pointer/cell.md) - - [Rc与Arc todo](advance/smart-pointer/rc-refcell.md) + - [Rc与Arc同一数据多所有者](advance/smart-pointer/rc-arc.md) + - [Cell与RefCell todo](advance/smart-pointer/cell-refcell.md) - [自引用与内存泄漏 todo](advance/smart-pointer/self-referrence.md) - [全局变量 todo](advance/global-variable.md) - [多线程 todo](advance/multi-threads/intro.md) @@ -94,7 +94,7 @@ - [可变借用失败引发的深入思考](pitfalls/multiple-mutable-references.md) - [不太勤快的迭代器](pitfalls/lazy-iterators.md) - [奇怪的序列x..y](pitfalls/weird-ranges.md) - + - [无处不在的迭代器](pitfalls/iterator-everywhere.md) - [对抗编译检查 doing](fight-with-compiler/intro.md) - [幽灵数据(todo)](fight-with-compiler/phantom-data.md) diff --git a/course-book/contents/advance/smart-pointer/cell-refcell.md b/course-book/contents/advance/smart-pointer/cell-refcell.md new file mode 100644 index 00000000..412d77b5 --- /dev/null +++ b/course-book/contents/advance/smart-pointer/cell-refcell.md @@ -0,0 +1,4 @@ +# Cell和RefCell + + +https://ryhl.io/blog/temporary-shared-mutation/ \ No newline at end of file diff --git a/course-book/contents/advance/smart-pointer/cell.md b/course-book/contents/advance/smart-pointer/cell.md deleted file mode 100644 index de1504f0..00000000 --- a/course-book/contents/advance/smart-pointer/cell.md +++ /dev/null @@ -1,4 +0,0 @@ -# Cell todo - - -https://ryhl.io/blog/temporary-shared-mutation/ \ No newline at end of file diff --git a/course-book/contents/advance/smart-pointer/rc-arc.md b/course-book/contents/advance/smart-pointer/rc-arc.md new file mode 100644 index 00000000..700c43a2 --- /dev/null +++ b/course-book/contents/advance/smart-pointer/rc-arc.md @@ -0,0 +1,201 @@ +# Rc与Arc +Rust所有权机制要求一个值只能有一个所有者,在大多数情况下,都没有问题,但是考虑以下情况: + +- 在图数据结构中,多个边可能会拥有同一个节点,该节点直到没有边指向它时,才应该被释放清理 +- 在多线程中,多个线程可能会指向同一个数据,但是你受限于Rust的安全机制,你又无法同时的可变借用该数据 + +以上场景不是很常见,但是一旦遇到,就非常棘手,为了解决此类问题,Rust在所有权机制之外又引入了额外的措施来简化相应的实现:通过引用计数的方式,允许一个数据资源在同一时刻拥有多个所有者。 + +这种实现机制就是`Rc`和`Arc`,前者适用于单线程,后者适用于多线程。由于二者大部分时间都是相同,因此本章将以`Rc`作为讲解主体,对于`Arc`的不同之处,也将进行单独讲解。 + +## Rc +引用计数(reference counting),顾名思义,通过记录一个数据被引用的次数来确定该数据是否正在被使用。当引用次数归零时,就代表该数据不再被使用,因此可以被清理释放。 + +而`Rc`正是引用计数的英文缩写。当我们**希望在堆上分配一个对象供程序的多个部分使用且无法确定哪个部分最后一个结束时,就可以使用`Rc`成为数据值的所有者**,例如之前提到的多线程场景就非常适合。 + +下面是经典的所有权被转移导致报错的例子: +```rust +fn main() { + let s = String::from("hello, world"); + // s在这里被转移给a + let a = Box::new(s); + // 报错!此处继续尝试将s转移给b + let b = Box::new(s); +} +``` + +使用`Rc`就可以轻易解决: +```rust +use std::rc::Rc; +fn main() { + let a = Rc::new(String::from("hello, world")); + let b = Rc::clone(&a); + + assert_eq!(2, Rc::strong_count(&a)); + assert_eq!(Rc::strong_count(&a),Rc::strong_count(&b)) +} +``` + +以上代码我们使用`Rc::new`创建了一个新的`Rc`智能指针并赋给变量`a`,该指针指向底层的字符串数据。 + +智能指针`Rc`在创建时,还会将引用计数加1,此时获取引用计数的关联函数`Rc::strong_count`返回的值将是`1`。 + +#### Rc::clone +接着,我们又使用`Rc::clone`克隆了一份智能指针`Rc`,并将该智能指针的引用计数增加到`2`。 + +由于`a`和`b`是同一个智能指针的两个副本,因此通过它们两个获取引用计数的结果都是`2`。 + +不要给`clone`字样所迷惑,以为所有的`clone`都是深拷贝。这里的`clone`**仅仅复制了智能指针并增加了引用计数,并没有克隆底层数据**,因此`a`和`b`是共享了底层的字符串`s`,这种**复制效率是非常高**的。当然你也可以使用`a.clone()`的方式来克隆,但是从可读性角度,`Rc::clone`的方式我们更加推荐。 + +实际上Rust中,还有不少`clone`都是浅拷贝,例如[迭代器的克隆](https://zhuanlan.zhihu.com/p/453149727). + +#### 观察引用计数的变化 +使用关联函数`Rc::strong_count`可以获取当前引用计数的值,我们来观察下引用计数如何随着变量声明、释放而变化: +```rust +use std::rc::Rc; +fn main() { + let a = Rc::new(String::from("test ref counting")); + println!("count after creating a = {}", Rc::strong_count(&a)); + let b = Rc::clone(&a); + println!("count after creating b = {}", Rc::strong_count(&a)); + { + let c = Rc::clone(&a); + println!("count after creating c = {}", Rc::strong_count(&c)); + } + println!("count after c goes out of scope = {}", Rc::strong_count(&a)); +} +``` + +有几点值得注意: + +- 由于变量`c`在语句块内部声明,当离开语句块时它会因为超出作用域而被释放,最终引用计数会减少1, 事实上这个得益于`Rc`实现了`Drop`特征 +- `a`,`b`,`c`三个智能指针引用计数都是同样的,并且共享底层的数据,因此打印计数时用哪个都行 +- 无法看到的是: 当`a`、`b`超出作用域后,引用计数会变成0,最终智能指针和它指向的底层字符串都会被清理释放 + +#### 不可变引用 +事实上,`Rc`是指向底层数据的不可变的引用,因此你无法通过它来修改数据,这也符合Rust的借用规则:要么多个不可变借用,要么一个可变借用。 + +但是可以修改数据也是非常有用的,只不过我们需要配合其它数据类型来一起使用,例如内部可变性的`RefCell`类型以及互斥锁`Mutex`。事实上,在多线程编程中,`Arc`跟`Mutext`锁的组合使用非常常见,既可以让我们在不同的线程中共享数据,又允许在各个线程中对其进行修改。 + + +#### 一个综合例子 +考虑一个场景,有很多小器具,里面每个器具都有自己的主人,但是存在多个器具属于同一个主人的情况,此时使用`Rc`就非常适合: +```rust +use std::rc::Rc; + +struct Owner { + name: String, + // ...other fields +} + +struct Gadget { + id: i32, + owner: Rc, + // ...other fields +} + +fn main() { + // 创建一个基于引用计数的`Owner`. + let gadget_owner: Rc = Rc::new( + Owner { + name: "Gadget Man".to_string(), + } + ); + + // 创建两个不同的工具,它们属于同一个主人 + let gadget1 = Gadget { + id: 1, + owner: Rc::clone(&gadget_owner), + }; + let gadget2 = Gadget { + id: 2, + owner: Rc::clone(&gadget_owner), + }; + + // 释放掉第一个`Rc` + drop(gadget_owner); + + // 尽管在之前我们释放了gadget_owner,但是依然可以在这里使用owner的信息 + // 原因是上面仅仅drop掉其中一个智能指针引用,而不是drop掉owner数据,外面还有两个引用指向底层的owner数据,引用计数尚未清零 + // 因此owner数据依然可以被使用 + println!("Gadget {} owned by {}", gadget1.id, gadget1.owner.name); + println!("Gadget {} owned by {}", gadget2.id, gadget2.owner.name); + + // 在函数最后,`gadget1`和`gadget2`也被释放,最终引用计数归零,随后底层数据也被清理释放 +} +``` + +以上代码很好的展示了`Rc`的用途,当然你也可以用借用的方式,但是实现起来就会复杂的多,而且随着工具在各个代码中的四处使用,引用生命周期也将变得更加复杂,毕竟结构体中的引用类型,总是令人不那么愉快,对不? + +#### Rc简单总结 + +- `Rc/Arc`是不可变引用,你无法修改它指向的值,只能进行读取, 如果要修改,需要配合后面章节的内部可变性`RefCell`或互斥锁`Mutex` +- 一旦最后一个拥有者消失,则资源会自动被回收,这个生命周期是在编译期就确定下来的 +- Rc只能用于同一线程内部,想要用于线程之间的对象共享, 你需要使用`Arc` +- `Rc`是一个智能指针,实现了`Deref`特征,因此你无需先解开`Rc`指针,再使用里面的`T`,而是可以直接使用`T`, 例如上例中的`gadget1.owner.name` + + + +## 多线程无力的Rc +来看看在多线程场景使用`Rc`会如何: +```rust +use std::rc::Rc; +use std::thread; + +fn main() { + let s = Rc::new(String::from("多线程漫游者")); + for _ in 0..10 { + let s = Rc::clone(&s); + let handle = thread::spawn(move || { + println!("{}",s) + }); + } +} +``` + +由于我们还没有学习多线程的章节,上面的例子就特地简化了相关的实现。首先通过`thread::spawn`创建一个线程,然后使用`move`关键字把克隆出的`s`的所有权转移到线程中。 + +能够实现这一点,完全得益于`Rc`带来的多所有权机制,但是以上代码会报错: +```console +error[E0277]: `Rc` cannot be sent between threads safely +``` + +表面原因是`Rc`不能在线程间安全的传递,实际上是因为它没有实现`Send`特征,而该特征是恰恰是多线程间传递数据的关键,我们会在多线程章节中进行讲解。 + +当然,还有更深层的原因: 由于`Rc`需要管理引用计数,但是该计数器并没有使用任何并发原语,因此无法实现原子化的计数操作, 最终会导致计数错误。 + +好在天无绝人之路,一起来看看Rust为我们提供的功能一致但是多线程安全的`Arc`。 + +## Arc +`Arc`是`Atomic Rc`的缩写,顾名思义:原子化的`Rc`智能指针。原子化是一种并发原语,我们在后续章节会进行深入讲解,这里你只要知道它能保证我们的数据能够安全的被线程间共享即可。 + +#### Arc的性能损耗 +你可能好奇,为何不直接使用`Arc`,还要画蛇添足弄一个`Rc`,还有Rust的基本数据类型、标准库数据类型为什么不自动实现原子化操作? + +原因在于原子化或者其它锁带来的线程安全,都会伴随着性能损耗,而且这种性能损耗还不小,因此Rust把这种选择权交给你,毕竟需要线程安全的代码其实占比并不高,大部分时间我们都在跟线程内的代码执行打交道。 + +`Arc`和`Rc`拥有完全一样的API,修改起来很简单: +```rust +use std::sync::Arc; +use std::thread; + +fn main() { + let s = Arc::new(String::from("多线程漫游者")); + for _ in 0..10 { + let s = Arc::clone(&s); + let handle = thread::spawn(move || { + println!("{}",s) + }); + } +} +``` + +对了,两者还有一点区别: `Arc`和`Rc`并没有定义在同一个模块,前者通过`use std::sync::Arc`来引入,后者`use std::rc::Rc`. + + +## 总结 +在Rust中,所有权机制保证了一个数据只会有一个所有者,如果你想要在图数据结构、或者多线程等中使用,这种机制会成为极大的阻碍。 好在Rust为我们提供了智能指针`Rc`和`Arc`,使用它们就能实现多个所有者共享一个数据的功能。 + +`Rc`和`Arc`的区别在于,后者是原子化实现的引用计数,因此是线程安全的,可以用于多线程中共享数据。 + +这两者都是只读的,如果想要实现内部数据可修改,必须配合内部可变性`RefCell`或者互斥锁`Mutex`来一起使用。 \ No newline at end of file diff --git a/course-book/contents/advance/smart-pointer/rc-refcell.md b/course-book/contents/advance/smart-pointer/rc-refcell.md deleted file mode 100644 index 2b8bcc3b..00000000 --- a/course-book/contents/advance/smart-pointer/rc-refcell.md +++ /dev/null @@ -1 +0,0 @@ -# Rc与RefCell(todo) diff --git a/course-book/contents/compiler/speed-up.md b/course-book/contents/compiler/speed-up.md index 4ad8fdec..6ad066fc 100644 --- a/course-book/contents/compiler/speed-up.md +++ b/course-book/contents/compiler/speed-up.md @@ -1,3 +1,7 @@ # 优化编译速度 -https://www.reddit.com/r/rust/comments/rnkyc0/why_does_my_code_compile_faster_on_nightly/ \ No newline at end of file +https://www.reddit.com/r/rust/comments/rnkyc0/why_does_my_code_compile_faster_on_nightly/ + +https://www.reddit.com/r/rust/comments/rv8126/speedup_compilation_time/ + +https://www.reddit.com/r/rust/comments/rsfcgb/why_is_my_rust_build_so_slow/ \ No newline at end of file diff --git a/course-book/contents/performance/cpu-cache.md b/course-book/contents/performance/cpu-cache.md index a0deb1c1..6a63a9e2 100644 --- a/course-book/contents/performance/cpu-cache.md +++ b/course-book/contents/performance/cpu-cache.md @@ -175,4 +175,8 @@ fn main() { let counter = Counters{c1: t1.recv().unwrap(),c2: t2.recv().unwrap()}; } ``` -It takes 2.03 ms to execute :) \ No newline at end of file +It takes 2.03 ms to execute :) + + +## 动态和静态分发 +https://www.reddit.com/r/rust/comments/ruavjm/is_there_a_difference_in_performance_between/ \ No newline at end of file diff --git a/course-book/contents/performance/intro.md b/course-book/contents/performance/intro.md index 5062ad24..97106f67 100644 --- a/course-book/contents/performance/intro.md +++ b/course-book/contents/performance/intro.md @@ -1 +1,4 @@ -# performance \ No newline at end of file +# performance + +## How do I profile a Rust web application in production? +https://www.reddit.com/r/rust/comments/rupcux/how_do_i_profile_a_rust_web_application_in/ \ No newline at end of file diff --git a/course-book/contents/performance/runtime-check.md b/course-book/contents/performance/runtime-check.md index 489d49f7..52543822 100644 --- a/course-book/contents/performance/runtime-check.md +++ b/course-book/contents/performance/runtime-check.md @@ -26,4 +26,7 @@ https://www.reddit.com/r/rust/comments/rntx7s/why_use_boxleak/ ## bounds check -https://www.reddit.com/r/rust/comments/rnbubh/whats_the_big_deal_with_bounds_checking/ \ No newline at end of file +https://www.reddit.com/r/rust/comments/rnbubh/whats_the_big_deal_with_bounds_checking/ + +## 使用assert 优化检查性能 +https://www.reddit.com/r/rust/comments/rui1zz/write_assertions_that_clarify_code_to_both_the/ \ No newline at end of file diff --git a/course-book/contents/pitfalls/iterator-everywhere.md b/course-book/contents/pitfalls/iterator-everywhere.md new file mode 100644 index 00000000..6caff6e3 --- /dev/null +++ b/course-book/contents/pitfalls/iterator-everywhere.md @@ -0,0 +1,107 @@ +# 无处不在的迭代器 +Rust的迭代器无处不在,直至你在它上面栽了跟头,经过深入调查才发现:哦,原来是迭代器的锅。不信的话,看看这个报错你能想到是迭代器的问题吗: `borrow of moved value: words`. + +## 报错的代码 +以下的代码非常简单,用来统计文本中字词的数量,并打印出来: +```rust +fn main() { + let s = "hello world"; + let mut words = s.split(" "); + let n = words.count(); + println!("{:?}",words); +} +``` + +四行代码,行云流水,一气呵成,且看成效: +```console +error[E0382]: borrow of moved value: `words` + --> src/main.rs:5:21 + | +3 | let mut words = s.split(" "); + | --------- move occurs because `words` has type `std::str::Split<'_, &str>`, which does not implement the `Copy` trait +4 | let n = words.count(); + | ------- `words` moved due to this method call +5 | println!("{:?}",words); + | ^^^^^ value borrowed here after move +``` + +世事难料,我以为只有的生命周期、闭包才容易背叛革命,没想到一个你浓眉大眼的`count`方法也背叛革命。从报错来看,是因为`count`方法拿走了`words`的所有权,来看看签名: +```rust +fn count(self) -> usize +``` +从签名来看,编译器的报错是正确的,但是为什么?为什么一个简单的标准库`count`方法就敢拿走所有权? + +## 迭代器回顾 +在[迭代器](../advance/functional-programing/iterator.md#消费者与适配器)章节中,我们曾经学习过两个概念:迭代器适配器和消费者适配器,前者用于对迭代器中的元素进行操作,最终生成一个新的迭代器,例如`map`、`filter`等方法;而后者用于消费掉迭代器,最终产生一个结果,例如`collect`方法, 一个典型的示例如下: +```rust +let v1: Vec = vec![1, 2, 3]; + +let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); + +assert_eq!(v2, vec![2, 3, 4]); +``` + +在其中,我们还提到一个细节,消费者适配器会拿走迭代器的所有权,那么这个是否与我们最开始碰到的问题有关系? + +## 深入调查 +要解释这个问题,必须要找到`words`是消费者适配器的证据,因此我们需要深入源码进行查看。 + +其实。。也不需要多深,只要进入`words`的源码,就能看出它属于`Iterator`特征,那说明`split`方法产生了一个迭代器?再来看看: +```rust +pub fn split<'a, P>(&'a self, pat: P) -> Split<'a, P> +where + P: Pattern<'a>, +//An iterator over substrings of this string slice, separated by characters matched by a pattern. +``` + +还真是,从代码注释来看,`Split`就是一个迭代器类型,用来迭代被分隔符隔开的子字符串集合。 + +真相大白了,`split`产生一个迭代器,而`count`方法是一个消费者适配器,用于消耗掉前者产生的迭代器,最终生成字词统计的结果。 + +本身问题不复杂,但是在**解决方法上,可能还有点在各位客官的意料之外**,且看下文。 + + +## 最rusty的解决方法 +你可能会想用`collect`来解决这个问题,先收集成一个集合,然后进行统计。当然此方法完全可行,但是很不`rusty`(很符合rust规范、潮流的意思),以下给出最`rusty`的解决方案: +```rust +let words = s.split(","); +let n = words.clone().count(); +``` + +在继续之前,我得先找一个地方藏好,因为俺有一个感觉,烂西红柿正在铺天盖地的呼啸而来,伴随而来的是读者的正义呵斥: +**你管`clone`叫最好、最`rusty`的解决方法??** + +大家且听我慢慢道来,事实上,在Rust中`clone`不总是性能低下的代名词,因为`clone`的行为完全取决于它的具体实现。 + +#### 迭代器的`clone`代价 +对于迭代器而言,它其实并不需要持有数据才能进行迭代,事实上它包含一个引用,该引用指向了保存在堆上的数据,而迭代器自身的结构是保存在栈上。 + +因此对迭代器的`clone`仅仅是复制了一份栈上的简单结构,性能非常高效,例如: +```rust +pub struct Split<'a, T: 'a, P> +where + P: FnMut(&T) -> bool, +{ + // Used for `SplitWhitespace` and `SplitAsciiWhitespace` `as_str` methods + pub(crate) v: &'a [T], + pred: P, + // Used for `SplitAsciiWhitespace` `as_str` method + pub(crate) finished: bool, +} + +impl Clone for Split<'_, T, P> +where + P: Clone + FnMut(&T) -> bool, +{ + fn clone(&self) -> Self { + Split { v: self.v, pred: self.pred.clone(), finished: self.finished } + } +} +``` + +以上代码实现了对`Split`迭代器的克隆,可以看出,底层的的数组`self.v`并没有被克隆而是简单的复制了一个引用,依然指向了底层的数组`&[T]`,因此这个克隆非常高效。 + +## 总结 +看起来是无效借用导致的错误,实际上是迭代器被消费了导致的问题,这说明Rust编译器虽然会告诉你错误原因,但是这个原因不总是根本原因。我们需要一双慧眼和勤劳的手,来挖掘出这个宝藏,最后为己所用。 + +同时,克隆在Rust中也并不总是**bad guy**的代名词,有的时候我们可以大胆去使用,当然前提是了解你的代码场景和具体的`clone`实现,这样你也能像文中那样作出非常`rusty`的选择。 \ No newline at end of file diff --git a/course-book/contents/practice/best-pratice.md b/course-book/contents/practice/best-pratice.md index 9b5303d8..3fa07e4a 100644 --- a/course-book/contents/practice/best-pratice.md +++ b/course-book/contents/practice/best-pratice.md @@ -7,4 +7,7 @@ https://www.reddit.com/r/rust/comments/rnmmqz/question_how_to_keep_code_dry_when https://www.reddit.com/r/rust/comments/rrgho1/what_is_the_recommended_way_to_use_a_library/ ## 最佳开发流程 -cargo watch \ No newline at end of file +cargo watch + +## 测试文件组织结构 +https://www.reddit.com/r/rust/comments/rsuhnn/need_a_piece_of_advice_about_organising_tests/ \ No newline at end of file diff --git a/course-book/writing-material/posts/atomic.md b/course-book/writing-material/posts/atomic.md new file mode 100644 index 00000000..0032cccd --- /dev/null +++ b/course-book/writing-material/posts/atomic.md @@ -0,0 +1 @@ +https://www.reddit.com/r/rust/comments/rtqrx4/introducing_atomicstory/ \ No newline at end of file diff --git a/course-book/writing-material/posts/threads.md b/course-book/writing-material/posts/threads.md index f950fa42..ab7ad107 100644 --- a/course-book/writing-material/posts/threads.md +++ b/course-book/writing-material/posts/threads.md @@ -24,4 +24,7 @@ fn main() { thread::sleep(Duration::from_millis(500)); } } -``` \ No newline at end of file +``` + +## 多个线程同时无锁的对一个数组进行修改 +https://www.reddit.com/r/rust/comments/rtutr0/lockless_threads_for_mutable_operations/ \ No newline at end of file From 47a87e6fbaa64a15e3f89fb4cc4c66b2c39ef1ec Mon Sep 17 00:00:00 2001 From: mg-chao Date: Wed, 5 Jan 2022 14:53:27 +0800 Subject: [PATCH 4/5] update: translate primitive types --- exercise/exercises/primitive_types/README.md | 7 ++-- .../primitive_types/primitive_types1.rs | 10 ++--- .../primitive_types/primitive_types2.rs | 16 ++++---- .../primitive_types/primitive_types3.rs | 8 ++-- .../primitive_types/primitive_types4.rs | 4 +- .../primitive_types/primitive_types5.rs | 6 +-- .../primitive_types/primitive_types6.rs | 10 ++--- exercise/info.toml | 39 ++++++++----------- 8 files changed, 47 insertions(+), 53 deletions(-) diff --git a/exercise/exercises/primitive_types/README.md b/exercise/exercises/primitive_types/README.md index cea69b02..8ff09a7e 100644 --- a/exercise/exercises/primitive_types/README.md +++ b/exercise/exercises/primitive_types/README.md @@ -1,9 +1,8 @@ -# Primitive Types +# 基本类型(Primitive Types) -Rust has a couple of basic types that are directly implemented into the -compiler. In this section, we'll go through the most important ones. +Rust 有几个直接在编译器中实现基本类型。在本节中,我们来看看最重要的几个。 -## Further information +## 更多信息 - [Data Types](https://doc.rust-lang.org/stable/book/ch03-02-data-types.html) - [The Slice Type](https://doc.rust-lang.org/stable/book/ch04-03-slices.html) diff --git a/exercise/exercises/primitive_types/primitive_types1.rs b/exercise/exercises/primitive_types/primitive_types1.rs index 09121392..2ab9ed61 100644 --- a/exercise/exercises/primitive_types/primitive_types1.rs +++ b/exercise/exercises/primitive_types/primitive_types1.rs @@ -1,6 +1,6 @@ // primitive_types1.rs -// Fill in the rest of the line that has code missing! -// No hints, there's no tricks, just get used to typing these :) +// 补充不完整代码行的缺失部分! +// 没有提示,也没有什么诀窍,只要习惯于键入这些内容就可以了 :) // I AM NOT DONE @@ -9,11 +9,11 @@ fn main() { let is_morning = true; if is_morning { - println!("Good morning!"); + println!("Good morning!");// 译:"早上好" } - let // Finish the rest of this line like the example! Or make it be false! + let // 参照上面的示例来补充这一行的缺失部分!或者让它值为 false ! if is_evening { - println!("Good evening!"); + println!("Good evening!");// 译:"晚上好" } } diff --git a/exercise/exercises/primitive_types/primitive_types2.rs b/exercise/exercises/primitive_types/primitive_types2.rs index 6576a4d5..506ae11e 100644 --- a/exercise/exercises/primitive_types/primitive_types2.rs +++ b/exercise/exercises/primitive_types/primitive_types2.rs @@ -1,6 +1,6 @@ // primitive_types2.rs -// Fill in the rest of the line that has code missing! -// No hints, there's no tricks, just get used to typing these :) +// 补充不完整代码行的缺失部分! +// 没有提示,也没有什么诀窍,只要习惯于键入这些内容就可以了 :) // I AM NOT DONE @@ -9,16 +9,16 @@ fn main() { let my_first_initial = 'C'; if my_first_initial.is_alphabetic() { - println!("Alphabetical!"); + println!("Alphabetical!");// 译:"字母!" } else if my_first_initial.is_numeric() { - println!("Numerical!"); + println!("Numerical!");// 译:"数字!" } else { - println!("Neither alphabetic nor numeric!"); + println!("Neither alphabetic nor numeric!");// 译:"既不是字母也不是数字!" } - let // Finish this line like the example! What's your favorite character? - // Try a letter, try a number, try a special character, try a character - // from a different language than your own, try an emoji! + let // 像上面的示例一样完成这行代码!你最喜欢的角色是什么? + // 试试一个字母,或者一个数字,也可以一个特殊字符,又或者一个不属于 + // 你母语的字符,一个表情符号看起来也不错。 if your_character.is_alphabetic() { println!("Alphabetical!"); } else if your_character.is_numeric() { diff --git a/exercise/exercises/primitive_types/primitive_types3.rs b/exercise/exercises/primitive_types/primitive_types3.rs index aaa518be..9d529db3 100644 --- a/exercise/exercises/primitive_types/primitive_types3.rs +++ b/exercise/exercises/primitive_types/primitive_types3.rs @@ -1,6 +1,6 @@ // primitive_types3.rs -// Create an array with at least 100 elements in it where the ??? is. -// Execute `rustlings hint primitive_types3` for hints! +// 在 ??? 处创建一个不少于 100 个元素的数组。 +// 执行 `rustex hint primitive_types3` 获取提示! // I AM NOT DONE @@ -8,8 +8,8 @@ fn main() { let a = ??? if a.len() >= 100 { - println!("Wow, that's a big array!"); + println!("Wow, that's a big array!");// 译:"哇!那数组可真大!" } else { - println!("Meh, I eat arrays like that for breakfast."); + println!("Meh, I eat arrays like that for breakfast.");// 译:"嗯,我把这样的数组当早餐吃。" } } diff --git a/exercise/exercises/primitive_types/primitive_types4.rs b/exercise/exercises/primitive_types/primitive_types4.rs index 10b553e9..8f102e6d 100644 --- a/exercise/exercises/primitive_types/primitive_types4.rs +++ b/exercise/exercises/primitive_types/primitive_types4.rs @@ -1,6 +1,6 @@ // primitive_types4.rs -// Get a slice out of Array a where the ??? is so that the test passes. -// Execute `rustlings hint primitive_types4` for hints!! +// 在 ??? 处获取数组 a 的一个切片(slice),以通过测试。 +// 执行 `rustex hint primitive_types4` 获取提示!! // I AM NOT DONE diff --git a/exercise/exercises/primitive_types/primitive_types5.rs b/exercise/exercises/primitive_types/primitive_types5.rs index 680d8d23..b5239838 100644 --- a/exercise/exercises/primitive_types/primitive_types5.rs +++ b/exercise/exercises/primitive_types/primitive_types5.rs @@ -1,12 +1,12 @@ // primitive_types5.rs -// Destructure the `cat` tuple so that the println will work. -// Execute `rustlings hint primitive_types5` for hints! +// 对 `cat` 元组进行解构(Destructure),使 println 能够运行。 +// 执行 `rustex hint primitive_types5` 获取提示! // I AM NOT DONE fn main() { let cat = ("Furry McFurson", 3.5); - let /* your pattern here */ = cat; + let /* your pattern here */ = cat;// 译:模式写在这 println!("{} is {} years old.", name, age); } diff --git a/exercise/exercises/primitive_types/primitive_types6.rs b/exercise/exercises/primitive_types/primitive_types6.rs index b8c9b82b..3fdd59b2 100644 --- a/exercise/exercises/primitive_types/primitive_types6.rs +++ b/exercise/exercises/primitive_types/primitive_types6.rs @@ -1,16 +1,16 @@ // primitive_types6.rs -// Use a tuple index to access the second element of `numbers`. -// You can put the expression for the second element where ??? is so that the test passes. -// Execute `rustlings hint primitive_types6` for hints! +// 使用元组索引(tuple index)来访问 `numbers` 的第二个元素。 +// 你可以把第二个元素的表达式放在 ??? 处,这样测试就会通过。 +// 执行 `rustex hint primitive_types6` 获取提示! // I AM NOT DONE #[test] fn indexing_tuple() { let numbers = (1, 2, 3); - // Replace below ??? with the tuple indexing syntax. + // 用元组索引的语法替换下面的 ??? let second = ???; assert_eq!(2, second, - "This is not the 2nd number in the tuple!") + "This is not the 2nd number in the tuple!")// 译:这不是元组中的第二个数字! } diff --git a/exercise/info.toml b/exercise/info.toml index d94f55e2..8b194743 100644 --- a/exercise/info.toml +++ b/exercise/info.toml @@ -212,40 +212,37 @@ https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-ref name = "primitive_types1" path = "exercises/primitive_types/primitive_types1.rs" mode = "compile" -hint = "No hints this time ;)" +hint = "这次没有提示 ;)" [[exercises]] name = "primitive_types2" path = "exercises/primitive_types/primitive_types2.rs" mode = "compile" -hint = "No hints this time ;)" +hint = "这次没有提示 ;)" [[exercises]] name = "primitive_types3" path = "exercises/primitive_types/primitive_types3.rs" mode = "compile" hint = """ -There's a shorthand to initialize Arrays with a certain size that does not -require you to type in 100 items (but you certainly can if you want!). -For example, you can do: +有一种简便的方法可以初始化具有一定大小的数组,而不需要你输入 100 个 +元素(但如果你想的话,那当然可以!)。 +例如,你可以这样做: let array = ["Are we there yet?"; 10]; -Bonus: what are some other things you could have that would return true -for `a.len() >= 100`?""" +额外目标: 还有哪些东西可以在 `a.len()>=100` 时返回 true """ [[exercises]] name = "primitive_types4" path = "exercises/primitive_types/primitive_types4.rs" mode = "test" hint = """ -Take a look at the Understanding Ownership -> Slices -> Other Slices section of the book: -https://doc.rust-lang.org/book/ch04-03-slices.html -and use the starting and ending indices of the items in the Array -that you want to end up in the slice. +看看这本书的:Understanding Ownership -> Slices -> Other Slices 章节吧: +https://doc.rust-lang.org/book/ch04-03-slices.html, +然后找出所需切片元素对应数组里的起始和截止下标。 -If you're curious why the first argument of `assert_eq!` does not -have an ampersand for a reference since the second argument is a -reference, take a look at the Deref coercions section of the book: +如果你好奇既然 `assert_eq!` 的第二个参数是引用,为什么第一个参数 +没有使用 & 号用来表示引用,可以看看这本书的 Deref 强制转换部分: https://doc.rust-lang.org/book/ch15-02-deref.html""" [[exercises]] @@ -253,22 +250,20 @@ name = "primitive_types5" path = "exercises/primitive_types/primitive_types5.rs" mode = "compile" hint = """ -Take a look at the Data Types -> The Tuple Type section of the book: +看看这本书的 Data Types -> The Tuple Type 类型章节: https://doc.rust-lang.org/book/ch03-02-data-types.html#the-tuple-type -Particularly the part about destructuring (second to last example in the section). -You'll need to make a pattern to bind `name` and `age` to the appropriate parts -of the tuple. You can do it!!""" +特别是关于解构的部分(这节中倒数第二个例子)。 +你需要一个模式将 `name` 和 `age` 绑定到元组的适当部分。你能够做到的!!""" [[exercises]] name = "primitive_types6" path = "exercises/primitive_types/primitive_types6.rs" mode = "test" hint = """ -While you could use a destructuring `let` for the tuple here, try -indexing into it instead, as explained in the last example of the -Data Types -> The Tuple Type section of the book: +虽然你可以使用 `let` 对元组进行解构 ,但不妨试试对它进行索引, +正如这本书的 Data Types -> The Tuple Type 部分的最后一个例子表示的那样。 https://doc.rust-lang.org/book/ch03-02-data-types.html#the-tuple-type -Now you have another tool in your toolbox!""" +现在,你的工具箱里又多了一个工具!""" # STRUCTS From 4739c8dcf2115342268d37f88671ac71f9655b6d Mon Sep 17 00:00:00 2001 From: sunface Date: Wed, 5 Jan 2022 20:16:54 +0800 Subject: [PATCH 5/5] add cell/refcell --- course-book/contents/SUMMARY.md | 11 +- .../advance/smart-pointer/cell-refcell.md | 353 +++++++++++++++++- .../pitfalls/refcell-compilation-error.md | 8 + 3 files changed, 366 insertions(+), 6 deletions(-) create mode 100644 course-book/contents/pitfalls/refcell-compilation-error.md diff --git a/course-book/contents/SUMMARY.md b/course-book/contents/SUMMARY.md index 029c8e84..e2b61a35 100644 --- a/course-book/contents/SUMMARY.md +++ b/course-book/contents/SUMMARY.md @@ -68,9 +68,9 @@ - [Box堆对象分配](advance/smart-pointer/box.md) - [Deref解引用](advance/smart-pointer/deref.md) - [Drop释放资源](advance/smart-pointer/drop.md) - - [Rc与Arc同一数据多所有者](advance/smart-pointer/rc-arc.md) - - [Cell与RefCell todo](advance/smart-pointer/cell-refcell.md) - - [自引用与内存泄漏 todo](advance/smart-pointer/self-referrence.md) + - [Rc与Arc实现1vN所有权机制](advance/smart-pointer/rc-arc.md) + - [Cell与RefCell内部可变性](advance/smart-pointer/cell-refcell.md) + - [Weak与自引用类型 todo](advance/smart-pointer/self-referrence.md) - [全局变量 todo](advance/global-variable.md) - [多线程 todo](advance/multi-threads/intro.md) - [线程管理 todo](advance/multi-threads/thread.md) @@ -85,7 +85,7 @@ - [一些写代码的技巧 todo](practice/coding-tips.md) - [最佳实践 todo](practice/best-pratice.md) -- [Rust陷阱系列](pitfalls/index.md) +- [Rust陷阱系列(持续更新)](pitfalls/index.md) - [for循环中使用外部数组](pitfalls/use-vec-in-for.md) - [线程类型导致的栈溢出](pitfalls/stack-overflow.md) - [算术溢出导致的panic](pitfalls/arithmetic-overflow.md) @@ -95,8 +95,9 @@ - [不太勤快的迭代器](pitfalls/lazy-iterators.md) - [奇怪的序列x..y](pitfalls/weird-ranges.md) - [无处不在的迭代器](pitfalls/iterator-everywhere.md) + - [编译期报错的RefCell todo](pitfalls/refcell-compilation-error.md) -- [对抗编译检查 doing](fight-with-compiler/intro.md) +- [对抗编译检查(持续更新)](fight-with-compiler/intro.md) - [幽灵数据(todo)](fight-with-compiler/phantom-data.md) - [生命周期)](fight-with-compiler/lifetime/intro.md) - [生命周期过大-01](fight-with-compiler/lifetime/too-long1.md) diff --git a/course-book/contents/advance/smart-pointer/cell-refcell.md b/course-book/contents/advance/smart-pointer/cell-refcell.md index 412d77b5..b83158e3 100644 --- a/course-book/contents/advance/smart-pointer/cell-refcell.md +++ b/course-book/contents/advance/smart-pointer/cell-refcell.md @@ -1,4 +1,355 @@ # Cell和RefCell +Rust的编译器之严格,可以说是举世无双。特别是在所有权方面,Rust通过严格的规则来保证所有权和借用的正确性,最终为程序的安全保驾护航。 +但是严格是一把双刃剑,带来安全提升的同时,损失了灵活性,有时甚至会让用户痛苦不堪、怨声载道。因此Rust提供了`Cell`和`RefCell`用于内部可变性, 简而言之,可以在拥有不可变引用的同时修改目标数据,对于正常的代码实现来说,这个是不可能做到的(要么一个可变借用,要么多个不可变借用). + +> 内部可变性的实现是因为Rust使用了`unsafe`来做到这一点,但是对于使用者来说,这些都是透明的,因为这些不安全代码都被封装到了安全的API中 + +## Cell +Cell和RefCell在功能上没有区别,区别在于`Cell`适用于`T`实现`Copy`的情况: +```rust +use std::cell::Cell; +fn main() { + let c = Cell::new("asdf"); + let one = c.get(); + c.set("qwer"); + let two = c.get(); + println!("{},{}", one,two); +} +``` + +以上代码展示了`Cell`的基本用法,有几点值得注意: + +- "asdf"是`&str`类型,它实现了`Copy`特征 +- `c.get`用来取值,`c.set`用来设置新值 + +取到值保存在`one`变量后,还能同时进行修改,这个违背了Rust的借用规则,但是由于`Cell`的存在,我们很优雅的做到了这一点,但是如果你尝试在`Cell`中存放`String`: +```rust + let c = Cell::new(String::from("asdf")); + ``` + +编译器会立刻报错,因为`String`没有实现`Copy`特征: +```console +| pub struct String { +| ----------------- doesn't satisfy `String: Copy` +| += note: the following trait bounds were not satisfied: + `String: Copy` +``` + +## RefCell +由于`Cell`类型针对的是实现了`Copy`特征的值类型,因此在实际开发中,`Cell`使用的并不多,因为我们要解决的往往是可变、不可变引用共存导致的问题,此时就需要借助于`RefCell`来达成目的。 + +我们可以将所有权、借用规则与这些智能指针做一个对比: + +| Rust规则 | 智能指针带来的额外规则 | +|--------|-------------| +| 一个数据只有一个所有者| `Rc/Arc`让一个数据可以拥有多个所有者 | +| 要么多个不可变借用,要么一个可变借用 | `RefCell`实现编译期可变、不可变引用共存 | +| 违背规则导致**编译错误** | 违背规则导致**运行时`panic`** | + +可以看出,`Rc/Arc`和`RefCell`合在一起,解决了Rust中严苛的所有权和借用规则带来的某些场景下难使用的问题。但是它们并不是银弹,例如`RefCell`实际上并没有解决可变引用和引用可以共存的问题,只是将报错从编译期推迟到运行时,从编译器错误变成了`panic`异常: +```rust +use std::cell::RefCell; + +fn main() { + let s = RefCell::new(String::from("hello, world")); + let s1 = s.borrow(); + let s2 = s.borrow_mut(); + + println!("{},{}",s1,s2); +} +``` + +上面代码在编译期不会报任何错误,你可以顺利运行程序: +```console +thread 'main' panicked at 'already borrowed: BorrowMutError', src/main.rs:6:16 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +``` + +但是依然会因为违背了借用规则导致了运行期`panic`,这非常像中国的天网,它也许会被罪犯蒙蔽一时,但是并不会被蒙蔽一世,任何导致安全风险的存在都将不能被容忍,法网恢恢,疏而不漏。 + +#### RefCell为何存在 +相信肯定有读者有疑问了,这么做有任何意义吗?还不如在编译期报错,至少能提前发现问题,而且性能还更好。 + +存在即合理,究其根因,在于Rust编译期的**宁可错杀,绝不放过**的原则, 当编译器不能确定你的代码是否正确时,就统统会判定为错误,因此难免会导致一些误报。 + +而`RefCell`正是**用于你确信代码是正确的,而编译器却发生了误判时**。 + +对于大型的复杂程序,也可以选择使用`RefCell`来让事情简化。例如在Rust编译器的[`ctxt结构体`](https://github.com/rust-lang/rust/blob/620d1ee5346bee10ba7ce129b2e20d6e59f0377d/src/librustc/middle/ty.rs#L803-L987)中有大量的`RefCell`类型的`map`字段, 主要的原因是:这些`map`会被分散在各个地方的代码片段所广泛使用或修改。由于这种分散在各处的使用方式,导致了管理可变和不可变成为一件非常复杂的任务(甚至不可能),你很容易就碰到编译器抛出来的各种错误。而且`RefCell`的运行时错误在这种情况下也变得非常可爱:一旦有人做了不正确的使用,代码会`panic`,然后告诉我们哪些借用冲突了。 + +总之,当你确信编译器误报但不知道该如何解决时,或者你有一个引用类型,需要被四处使用和修改然后导致借用关系难以管理时,都可以优先考虑使用`RefCell`。 + + +#### RefCell简单总结 + +- 与Cell用于可Copy的值不同,RefCell用于引用 +- RefCell只是将借用规则从编译期推迟到程序运行期,并不能帮你绕过这个规则 +- RefCell适用于编译期误报或者一个引用被在多个代码中使用、修改以至于难于管理借用关系时 +- 使用`RefCell`时,违背借用规则会导致运行期的`panic` + +## 选择`Cell`还是`RefCell` +根据本文的内容,我们可以大概总结下两者的区别: + +- `Cell`只适用于`Copy`类型,用于提供值, 而`RefCell`用于提供引用 +- `Cell`不会`panic`,而`RefCell`会 + +#### 性能比较 +`Cell`没有额外的性能损耗,例如以下两段代码的性能其实是一致的: +```rust +// code snipet 1 +let x = Cell::new(1); +let y = &x; +let z = &x; +x.set(2); +y.set(3); +z.set(4); +println!("{}", x.get()); + +// code snipet 2 +let mut x = 1; +let y = &mut x; +let z = &mut x; +x = 2; +*y = 3; +*z = 4; +println!("{}", x; +``` + +虽然性能一致,但代码`1`拥有代码`2`不具有的优势:它能编译成功:) + +与`Cell`的`zero cost`不同,`RefCell`其实是有一点运行期开销的,原因是它包含了一个字大小的"借用状态"指示器,该指示器在每次运行时借用时都会被修改,进而产生一点开销。 + + +总之,当非要使用内部可变性时,首选`Cell`,只有值拷贝的方式不能满足你时,才去选择`RefCell`。 + + +## 内部可变性 +之前我们提到RefCell具有内部可变性,何为内部可变性?简单来说,对一个不可变的值进行可变借用,但这个并不符合Rust的基本借用规则: +```rust +fn main() { + let x = 5; + let y = &mut x; +} +``` + +上面的代码会报错,因为我们不能对一个不可变的值进行可变借用,这会破坏Rust的安全性保证,相反,你可以对一个可变值进行不可变借用。原因是:当值不可变时,可能会有多个不可变的引用指向它,修改其中一个为可变的,会造成可变引用与不可变引用共存的情况;而当值可变时,只会有唯一一个可变引用指向它,将其修改为不可变,那么最终依然是只有一个不可变的引用指向它。 + +虽然基本借用规则是Rust的基石,然而在某些场景中,一个值可以在其方法内部被修改,同时对于其它代码不可变,是很有用的: +```rust +// 定义在外部库中的特征 +pub trait Messenger { + fn send(&self, msg: String); +} + +// -------------------------- +// 我们的代码中的数据结构和实现 +struct MsgQueue { + msg_cache: Vec, +} + +impl Messenger for MsgQueue { + fn send(&self,msg: String) { + self.msg_cache.push(msg) + } +} +``` + +如上所示,外部库中定义了一个消息发送器特征`Messenger`,它就一个功能用于发送消息: `fn send(&self, msg: String)`,因为发送消息不需要修改自身,因此原作者在定义时,使用了`&self`的不可变借用, 这个无可厚非。 + +但是问题来了,我们要在自己的代码中使用该特征实现一个异步消息队列,出于性能的考虑,消息先写到本地缓存(内存)中,然后批量发送出去,因此在`send`方法中,需要将消息先行插入到本地缓存`msg_cache`中。但是问题来了,该`send`方法的签名是`&self`,因此上述代码会报错: +```console +error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference + --> src/main.rs:11:9 + | +2 | fn send(&self, msg: String); + | ----- help: consider changing that to be a mutable reference: `&mut self` +... +11 | self.sent_messages.push(msg) + | ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable +``` + +在报错的同时,编译器大聪明还善意的给出了提示:将`&self`修改为`&mut self`,但是。。。我们实现的特征是定义在外部库中,因此该签名根本不能修改。值此危急关头,`RefCell`闪亮登场: +```rust +use std::cell::RefCell; +pub trait Messenger { + fn send(&self, msg: String); +} + +pub struct MsgQueue { + msg_cache: RefCell>, +} + +impl Messenger for MsgQueue { + fn send(&self,msg: String) { + self.msg_cache.borrow_mut().push(msg) + } +} + +fn main() { + let mq = MsgQueue{msg_cache: RefCell::new(Vec::new())}; + mq.send("hello, world".to_string()); +} +``` + +这个MQ功能很弱,但是并不妨碍我们演示内部可变性的核心用法:通过包裹一层`RefCell`,成功的让`&self`中的`msg_cache`成为一个可变值,然后实现对其的修改。 + +## Rc + RefCell组合使用 +在Rust中,一个常见的组合就是`Rc`和`RefCell`在一起使用,前者可以实现一个数据拥有多个所有者,后者可以实现数据的可变性: +```rust +use std::cell::RefCell; +use std::rc::Rc; +fn main() { + let s = Rc::new(RefCell::new("我很善变,还拥有多个主人".to_string())); + + let s1 = s.clone(); + let s2 = s.clone(); + // let mut s2 = .borrow_mut(); + s2.borrow_mut().push_str(", on yeah!"); + + println!("{:?}\n{:?}\n{:?}", s, s1, s2); +} + +``` + +上面代码中,我们使用`RefCell`包裹一个字符串,同时通过`Rc`创建了它的三个所有者:`s`,`s1`和`s2`,并且通过其中一个所有者`s2`对字符串内容进行了修改。 + +由于`Rc`的所有者们共享同一个底层的数据,因此当一个所有者修改了数据时,会导致全部所有者持有的数据都发生了变化。 + +程序的运行结果也在预料之中: +```console +RefCell { value: "我很善变,还拥有多个主人, on yeah!" } +RefCell { value: "我很善变,还拥有多个主人, on yeah!" } +RefCell { value: "我很善变,还拥有多个主人, on yeah!" } +``` + + +#### 性能损耗 +相信这两者组合在一起使用时,很多人会好奇到底性能如何,下面我们来简单分析下。 + +首先给出一个大概的结论,这两者结合在一起使用的性能其实非常高,大致相当于没有线程安全版本的C++ `std::shared_ptr`指针, 事实上,`C++`这个指针的主要开销也在于原子性这个并发原语上,毕竟线程安全在哪个语言中开销都不小。 + +#### 内存损耗 +两者结合的数据结构类似: +```rust +struct Wrapper { + // Rc + strong_count: usize, + weak_count: usize, + + // Refcell + borrow_count: isize, + + // 包裹的数据 + item: T, +} +``` + +从上面可以看出,从对内存的影响来看,仅仅多分配了三个`usize/isize`,并没有其它额外的负担。 + +#### CPU损耗 +从CPU来看,损耗如下: + +- 对`Rc`解引用是免费的(编译期), 但是*带来的间接取值并不免费 +- 克隆`Rc`需要将当前的引用计数跟`0`和`usize::Max`进行一次比较,然后将计数值加1 +- 释放(drop)`Rc`将计数值减1, 然后跟`0`进行一次比较 +- 对`RefCell`进行不可变借用,将`isize`类型的借用计数加1,然后跟`0`进行比较 +- 对`RefCell`的不可变借用进行释放,将`isize`减1 +- 对`RefCell`的可变借用大致流程跟上面差不多,但是是先跟`0`比较,然后再减1 +- 对`RefCell`的可变借用进行释放,将`isize`加1 + +其实这些细节不必过于关注,只要知道`CPU`消耗也非常低,甚至编译器还会对此进行进一步优化! + + +#### CPU缓存Miss +唯一需要担心的可能就是这种组合数据结构对于`CPU`缓存是否亲和,这个我们无法证明,只能提出来存在这个可能性,最终的性能影响还需要在实际场景中进行测试 + +总之,分析这两者组合的性能还挺复杂的,大概总结下: + +- 从表面来看,它们带来的内存和CPU损耗都不大 +- 但是由于`Rc`额外的引入了一次间接取值(*),在少数场景下可能会造成性能上的显著损失 +- CPU缓存可能也不够亲和 + +## 通过`Cell::from_mut`解决借用冲突 +在Rust1.37版本中新增了两个非常实用的方法: + +- Cell::from_mut, 该方法将`&mut T`转为`&Cell` +- Cell::as_slice_of_cells,该方法将`&Cell<[T]>`转为`&[Cell]` + +这里我们不做深入的介绍,但是来看看如何使用这两个方法来解决一个常见的借用冲突问题: +```rust +fn is_even(i: i32) -> bool { + i % 2 == 0 +} + +fn retain_even(nums: &mut Vec) { + let mut i = 0; + for num in nums.iter().filter(|&num| is_even(*num)) { + nums[i] = *num; + i += 1; + } + nums.truncate(i); +} +``` +以上代码会报错: +```console +error[E0502]: cannot borrow `*nums` as mutable because it is also borrowed as immutable + --> src/main.rs:8:9 + | +7 | for num in nums.iter().filter(|&num| is_even(*num)) { + | ---------------------------------------- + | | + | immutable borrow occurs here + | immutable borrow later used here +8 | nums[i] = *num; + | ^^^^ mutable borrow occurs here +``` + +很明显,因为同时借用了不可变与可变引用,你可以通过索引的方式来绕过: +```rust +fn retain_even(nums: &mut Vec) { + let mut i = 0; + for j in 0..nums.len() { + if is_even(nums[j]) { + nums[i] = nums[j]; + i += 1; + } + } + nums.truncate(i); +} +``` + +但是这样就违背我们的初衷了,而且迭代器会让代码更加简洁,还有其它的办法吗? + +这时就可以使用`Cell`新增的这两个方法: +```rust +use std::cell::Cell; + +fn retain_even(nums: &mut Vec) { + let slice: &[Cell] = Cell::from_mut(&mut nums[..]) + .as_slice_of_cells(); + + let mut i = 0; + for num in slice.iter().filter(|num| is_even(num.get())) { + slice[i].set(num.get()); + i += 1; + } + + nums.truncate(i); +} +``` + +此时代码将不会报错,因为`Cell`上的`set`方法获取的是不可变引用`pub fn set(&self, val: T) {`. + +当然,以上代码的本质还是对`Cell`的运用,只不过这两个方法可以很方便的帮我们把`&mut T`类型转换成`&[Cell]`类型。 + + +## 总结 +`Cell`和`RefCell`都为我们带来了内部可见性这个重要特性,同时还将借用规则的检查从编译期推迟到运行期,但是这个检查并不能被绕过,该来早晚还是会来,`R在运行期的报错会造成`panic` + +`RefCell`适用于编译器误报或者一个引用被在多个代码中使用、修改以至于难于管理借用关系时,还有就是需要内部可变性时。 + +从性能上看,`RefCell`由于是非线程安全的,因此无需保证原子性,性能虽然有一点损耗,但是依然非常好,而`Cell`则完全不存在任何额外的性能损耗。 + +`Rc`跟`RefCell`结合使用可以实现多个所有者共享同一份数据,非常好用,但是潜在的性能损耗也要考虑进去,建议对于热点代码使用时,做好`benchmark`. -https://ryhl.io/blog/temporary-shared-mutation/ \ No newline at end of file diff --git a/course-book/contents/pitfalls/refcell-compilation-error.md b/course-book/contents/pitfalls/refcell-compilation-error.md new file mode 100644 index 00000000..8d1dfbff --- /dev/null +++ b/course-book/contents/pitfalls/refcell-compilation-error.md @@ -0,0 +1,8 @@ +# 编译期报错的RefCell + +@todo + +https://stackoverflow.com/questions/67023741/mutating-fields-of-rc-refcell-depending-on-its-other-internal-fields + + +https://stackoverflow.com/questions/47060266/error-while-trying-to-borrow-2-fields-from-a-struct-wrapped-in-refcell \ No newline at end of file