Merge pull request #281 from 1132719438/main

Some fixes in smart-pointer and circle-ref chapter
pull/284/head
Sunface 3 years ago committed by GitHub
commit 9afb119b18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -91,7 +91,7 @@ thread 'main' has overflowed its stack
fatal runtime error: stack overflow
```
通过`a.tail`的调用Rust试图打印出`a -> b ->a···`的所有内容,但是在不懈的努力后,`main`线程终于不堪重负,发生了[栈溢出](https://zhuanlan.zhihu.com/p/446039229)。
通过`a.tail`的调用Rust试图打印出`a -> b ->a···`的所有内容,但是在不懈的努力后,`main`线程终于不堪重负,发生了[栈溢出](https://course.rs/pitfalls/stack-overflow.html)。
以上的代码可能并不会造成什么大的问题,但是在一个更加复杂的程序中,类似的问题可能会造成你的程序不断的分配内存、泄漏内存,最终程序会不幸**OOM**当然这其中的CPU损耗也不可小觑。
@ -279,9 +279,9 @@ fn main() {
这个例子就留给读者自己解读和分析,我们就不画蛇添足了:)
## unsafe解决循环引用
除了使用Rust标准库提供的这些类型你还可以使用`unsafe`里的原生指针来解决这些棘手的问题,但是由于我们还没有讲解`unsafe`,因此这里就不进行展开,只附上[源码链接](https://codes.rs/unsafe/self-ref.html), 挺长的需要耐心o,O
除了使用Rust标准库提供的这些类型你还可以使用`unsafe`里的原生指针来解决这些棘手的问题,但是由于我们还没有讲解`unsafe`,因此这里就不进行展开,只附上[源码链接](https://codes.rs/unsafe/self-ref.html), 挺长的需要耐心o_O
虽然`unsfae`不安全,但是在各种库的代码中依然很常见用它来实现自引用结构,主要优点如下:
虽然`unsafe`不安全,但是在各种库的代码中依然很常见用它来实现自引用结构,主要优点如下:
- 性能高,毕竟直接用原生指针操作
- 代码更简单更符合直觉: 对比下`Option<Rc<RefCell<Node>>>`

@ -196,7 +196,7 @@ fn main() {
运行后输出:
```console
hello, 0x16f3aec70
hello, world!, 0x16f3#aec70
hello, world!, 0x16f3aec70
```
上面的`unsafe`虽然简单好用,但是它不太安全,是否还有其他选择?还真的有,那就是`Pin`。
@ -254,12 +254,12 @@ fn main() {
上面的代码也非常清晰,虽然使用了`unsafe`,其实更多的是无奈之举,跟之前的`unsafe`实现完全不可同日而语。
其实`Pin`在这里并没有魔法,它也并不是实现自引用类型的主要原因,最关键的还是里面的原生指针的使用,而`Pin`起到的就是确保我们的值不会被移走,否则指针就会指向一个错误的地址!
其实`Pin`在这里并没有魔法,它也并不是实现自引用类型的主要原因,最关键的还是里面的原生指针的使用,而`Pin`起到的作用就是确保我们的值不会被移走,否则指针就会指向一个错误的地址!
## 使用ouroboros
对于自引用结构体,三方库也有支持的,其中一个就是`ouroboros`,当然它也有自己的限制,我们后面会提到,先来看看该如何使用:
对于自引用结构体,三方库也有支持的,其中一个就是[ouroboros](https://github.com/joshua-maros/ouroboros),当然它也有自己的限制,我们后面会提到,先来看看该如何使用:
```rust
use ouroboros::self_referencing;
@ -337,7 +337,7 @@ fn main() {
类似的库还有:
- [rental](https://github.com/jpernst/rental) 这个库其实是最有名的,但是好像不再维护了,用倒是没问题
- [owning-ref](https://github.com/Kimundi/owning-ref-rs)
- [owning-ref](https://github.com/Kimundi/owning-ref-rs) ,将所有者和它的引用绑定到一个封装类型
这三个库各有各的特点也各有各的缺陷建议大家需要时一定要仔细调研并且写demo进行测试不可大意。
@ -352,7 +352,7 @@ fn main() {
## 学习一本书:如何实现链表
最后,推荐一本专门将如何实现链表的书(真是富有Rust特色链表都能复杂到出书了O, O)[too many lists](https://rust-unofficial.github.io/too-many-lists/)
最后,推荐一本专门将如何实现链表的书(真是富有Rust特色链表都能复杂到出书了O, O)[Learn Rust by writing Entirely Too Many Linked Lists](https://rust-unofficial.github.io/too-many-lists/)
## 总结

@ -6,7 +6,7 @@
## Rust中的堆栈
高级语言Python/Java等往往会弱化堆栈的概念但是要用好C/C++/Rust就必须对堆栈有深入的了解原因是两者的内存管理方式不同: 前者有GC垃圾回收机制 因此无需你去关心内存的细节。
栈内存从高位地址向下增长,且栈内存是连续分配的,一般来说**操作系统对栈内存的大小都有限制**因此C语言中无法创建任意长度的数组。在Rust中, `main`线程的[栈大小是`8MB`](https://zhuanlan.zhihu.com/p/446039229),普通线程是`2MB`在函数调用时会在其中创建一个临时栈空间调用结束后Rust会让这个栈空间里的对象自动进入`Drop`流程,最后栈顶指针自动移动到上一个调用栈顶,无需程序员手动干预,因而栈内存申请和释放是非常高效的。
栈内存从高位地址向下增长,且栈内存是连续分配的,一般来说**操作系统对栈内存的大小都有限制**因此C语言中无法创建任意长度的数组。在Rust中, `main`线程的[栈大小是`8MB`](https://course.rs/pitfalls/stack-overflow.html),普通线程是`2MB`在函数调用时会在其中创建一个临时栈空间调用结束后Rust会让这个栈空间里的对象自动进入`Drop`流程,最后栈顶指针自动移动到上一个调用栈顶,无需程序员手动干预,因而栈内存申请和释放是非常高效的。
与栈相反,堆上内存则是从低位地址向上增长,**堆内存通常只受物理内存限制**,而且通常是不连续的, 因此从性能的角度看,栈往往比对堆更高。
@ -56,7 +56,7 @@ fn main() {
}
```
这样就可以创建一个智能指针指向了存储在堆上的`5`,并且`a`持有了该指针。在本章的引言中,我们提到了智能指针往往都实现了`Deref`和`Drop`特征,因此:
这样就可以创建一个智能指针指向了存储在堆上的`3`,并且`a`持有了该指针。在本章的引言中,我们提到了智能指针往往都实现了`Deref`和`Drop`特征,因此:
- `println!`可以正常打印出`a`的值,是因为它隐式的调用了`Deref`对智能指针`a`进行了解引用
- 最后一行代码`let b = a + 1`报错,是因为在表达式中,我们无法自动隐式的执行`Deref`解引用操作, 你需要使用`*`操作符`let b = *a + 1`,来显式的进行解引用

@ -113,7 +113,7 @@ let z = &mut x;
x = 2;
*y = 3;
*z = 4;
println!("{}", x;
println!("{}", x);
```
虽然性能一致,但代码`1`拥有代码`2`不具有的优势:它能编译成功:)
@ -204,7 +204,7 @@ fn main() {
let s1 = s.clone();
let s2 = s.clone();
// let mut s2 = .borrow_mut();
// let mut s2 = s.borrow_mut();
s2.borrow_mut().push_str(", on yeah!");
println!("{:?}\n{:?}\n{:?}", s, s1, s2);
@ -339,9 +339,9 @@ fn retain_even(nums: &mut Vec<i32>) {
}
```
此时代码将不会报错,因为`Cell`上的`set`方法获取的是不可变引用`pub fn set(&self, val: T) {`.
此时代码将不会报错,因为`Cell`上的`set`方法获取的是不可变引用`pub fn set(&self, val: T)`.
当然,以上代码的本质还是对`Cell`的运用,只不过这两个方法可以很方便的帮我们把`&mut T`类型转换成`&[Cell<T>]`类型。
当然,以上代码的本质还是对`Cell`的运用,只不过这两个方法可以很方便的帮我们把`&mut [T]`类型转换成`&[Cell<T>]`类型。
## 总结

@ -149,7 +149,7 @@ fn display(s: &str) {
```rust
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
display(&(*m)[..]);
}
```
@ -161,7 +161,7 @@ fn main() {
```rust
fn main() {
let s = MyBox::new(String::from("hello, world"));
let s1:&str = &s;
let s1: &str = &s;
let s2: String = s.to_string();
}
```
@ -171,14 +171,14 @@ fn main() {
## Deref规则总结
在上面,我们零碎的介绍了不少关于`Deref`特征的知识,下面来通过较为正式的方式来对其规则进行下总结。
一个类型为`T`的对象`foo`,如果`T: Deref<Target=U>`,那么,相关`foo`的引用`&foo`在应用的时候会自动转换`&U`。
一个类型为`T`的对象`foo`,如果`T: Deref<Target=U>`,那么,相关`foo`的引用`&foo`在应用的时候会自动转换`&U`。
粗看这条规则,貌似有点类似于`AsRef`,而跟`解引`似乎风马牛不相及, 实际里面里面有些玄妙之处。
粗看这条规则,貌似有点类似于`AsRef`,而跟`解引`似乎风马牛不相及, 实际里面有些玄妙之处。
Rust编译器会在做`*v`操作的时候,自动先把`v`做引用归一化操作,即转换成内部通用引用的形式`&v`,整个表达式就变成 `*&v`。这里面有两种情况:
1. 把智能指针比如在库中定义的Box, Rc, Arc, Cow 等),去掉壳,转成内部标准形式`&v`
2. 把多重`&` (比如:`&&&&&&&v`),简化成`&v`(通过插入足够数量的`*`进行解引)。
2. 把多重`&` (比如:`&&&&&&&v`),简化成`&v`(通过插入足够数量的`*`进行解引)。
所以,它实际上在解引用之前做了一个引用的归一化操作。
为什么要转呢? 因为编译器设计的能力是,只能够对 &v 这种引用进行解引用。其它形式的它不认识,所以要做引用归一化操作。
@ -215,7 +215,7 @@ Rust编译器会在做`*v`操作的时候,自动先把`v`做引用归一化操
foo(&counted);
```
因为`Vec<T>` 实现了`Deref<Target=[T]>`。
因为`Rc<T>` 实现了`Deref<Target=T>`。
```rust
struct Foo;
@ -283,7 +283,7 @@ fn display(s: &mut String) {
以上代码有几点值得注意:
- 要实现`DerefMut`必须要先实现`Deref`特征: `pub trait DerefMut: Deref {`
- 要实现`DerefMut`必须要先实现`Deref`特征: `pub trait DerefMut: Deref`
- `T: DerefMut<Target=U>`解读:将`&mut T`类型通过`DerefMut`特征的方法转换为`&mut U`类型,对应上例中,就是将`&mut MyBox<String>`转换为`&mut String`
对于上述三条规则中的第三条它比另外两条稍微复杂了点Rust可以把可变引用隐式的转换成不可变引用但反之则不行。

@ -7,7 +7,7 @@
## Rust中的资源回收
在一些无GC语言中程序员在一个变量无需再被使用时需要手动释放它占用的内存资源如果忘记了那么就会发生内存泄漏最终臭名昭著的`OOM`问题可能就会发生。
而在Rust中你可以指定在一个变量超出作用域时执行一段特定的代码最终编译器将帮你自动插入这段收尾代码。这样就无需在每一个使用该变量的地方都写一段代码来进行收尾工作和资源释放。不让人感叹Rust的大腿真粗
而在Rust中你可以指定在一个变量超出作用域时执行一段特定的代码最终编译器将帮你自动插入这段收尾代码。这样就无需在每一个使用该变量的地方都写一段代码来进行收尾工作和资源释放。不让人感叹Rust的大腿真粗
没错,指定这样一段收尾工作靠的就是咱这章的主角 - `Drop`特征。
@ -70,7 +70,7 @@ Dropping HasDrop2!
#### Drop的顺序
观察以上输出,我们可以得出以下关于`Drop`顺序的结论
- **变量级别,按照逆序的方式**,`_x`在`_foo`之前创建,因此`_x`在`_foo`之后被drop
- **变量级别,按照逆序的方式**,`_x`在`_foo`之前创建,因此`_x`在`_foo`之后被drop
- **结构体内部,按照顺序的方式**, 结构体`_x`中的字段按照定义中的顺序依次`drop`
#### 没有实现Drop的结构体
@ -152,7 +152,7 @@ Bingo完美拿走了所有权而且这种实现保证了后续的使用必
对于第二点,在之前我们已经详细介绍过,因此这里主要对第一点进行下简单说明。
在绝大多数情况下,我们都无需手动去`drop`以回收内存资源因为Rust会自动帮我们完成这些工作它甚至会对复杂类型的每个字段都单独的调用`drop`进行回收但是确实有极少数情况需要你自己来回收资源的例如文件描述符、网络socket等当这些超出作用域不再使用时就需要进行关闭以释放相关的资源在这些情况下就需要使用者自己来解决`Drop`的问题。
在绝大多数情况下,我们都无需手动去`drop`以回收内存资源因为Rust会自动帮我们完成这些工作它甚至会对复杂类型的每个字段都单独的调用`drop`进行回收但是确实有极少数情况需要你自己来回收资源的例如文件描述符、网络socket等当这些超出作用域不再使用时,就需要进行关闭以释放相关的资源,在这些情况下,就需要使用者自己来解决`Drop`的问题。
## 互斥的Copy和Drop

@ -47,7 +47,7 @@ fn main() {
不要给`clone`字样所迷惑,以为所有的`clone`都是深拷贝。这里的`clone`**仅仅复制了智能指针并增加了引用计数,并没有克隆底层数据**,因此`a`和`b`是共享了底层的字符串`s`,这种**复制效率是非常高**的。当然你也可以使用`a.clone()`的方式来克隆,但是从可读性角度,`Rc::clone`的方式我们更加推荐。
实际上Rust中还有不少`clone`都是浅拷贝,例如[迭代器的克隆](https://zhuanlan.zhihu.com/p/453149727).
实际上Rust中还有不少`clone`都是浅拷贝,例如[迭代器的克隆](https://course.rs/pitfalls/iterator-everywhere.html).
#### 观察引用计数的变化
使用关联函数`Rc::strong_count`可以获取当前引用计数的值,我们来观察下引用计数如何随着变量声明、释放而变化:

Loading…
Cancel
Save