前言#
昨天我们知道了如何发布一个库等
今天继续往下学
智能指针(smart pointers)#
突然又多了个概念--智能指针.
相信大家对指针都很了解了,存储一个地址,这个地址指向内存里的某个地方,那个地方存储着想要的数据.
我们之前学的引用(reference)就是这样, 引用算是指针的一种。
引用仅仅只是存储一个地址指向内存里的某个地址,没了,没有别的能力和消耗.
智能指针是另一个概念,而这个概念源于C++,在其他语言中也存在这一概念.
由于rust特有的所有权(ownership)和借用(borrow),这个智能指针又和别的语言有所不同.
rust开发团队对智能指针的描述是: 大多数时候不仅仅只是拥有数据的地址,也会拥有这个数据.
其实我们之前就有接触到智能指针,比如String和Vec,它们都可以算作是智能指针.
因为它们都拥有着某块内存并且允许修改.
它们同样拥有元数据(metadata)、额外的能力(extra capabilities)或者担保(guarantees).
比如String,把它的容量(capacity)作为它的元数据,还有额外的能力确保它的数据都是符合UTF-8要求的.
大多数时候智能指针都是使用struct来实现, 不过和一般的结构体不同的是,智能指针实现了Deref和Drop两个traits.
Deref这个trait允许智能指针的实例表现的和引用一样, 所以你可以把你的智能指针当作一个引用来使用.
Drop这个trait允许你自定义智能指针的实例超出作用域之后做的事情.
我们之后将会分析为什么一个智能指针需要实现这两个trait.
另外由于智能指针模式(pattern) 在rust中算是通用的设计模式(general design pattern),所以这章并不会也不能描述完所有的智能指针.
不过我们将接触到标准库中最常用的那几个
Box: 将值分配到heap也就是堆上Rc: 一个引用计数类型(reference counting type), 允许多个所有权(multiple ownership)Ref以及RefMut,通过RefCell来访问(accessed)它们RefCell: 强制借用(borrowing)运行在runtime,而不是原来的compile time也就是编译阶段.
除此另外
还有*interior mutability,*也就是内部可变性模式(pattern): 一个不可变类型,通过暴露一个api来允许修改内部的数据;
以及*reference cycles,*也就是引用循环: 它们是如何泄漏内存的,要如何防止这些情况.
使用Box指向heap上的数据#
一个Box类型的box就是一个最直接的智能指针,它允许你把数据存储到heap上,而非stack.而存储在stack上的是指向这个数据的指针.
有点拗口,我们梳理下:
原本数据放在stack上,现在通过Box转到heap上,而原本stack上的则变成了一个指向这个堆内存的指针.
boxes除了把数据放到heap上的开销之外,不会再有其余的性能开销. 不过它们也没有其它的能力(capabilities)了,它们大多都用于一下几个场景:
- 编译阶段你并不能明确你这个数据需要占的空间是多少,但是这个时候你用这个数据的上下文环境(
context)又要求数据是需要明确占用空间的. - 你有一大块数据准备转交(
transfer)所有权(请确保这个操作不会导致数据被复制一份). - 你想获取某个数据的所有权并且你并不要求这个数据的类型是某个确定的类型,只需要它实现了某个
trait即可.
第一个场景我们等会会遇到.
第二个场景: 如果一大块数据从stack上转交所有权,那么就需要触发copy的trait,这将会耗费非常多的时间,所以我们可以通过box将这一大块数据放到heap上,仅保留少部分引用在stack上,这样复制起来就很快,因为只是复制指针.
第三个场景: 在rust中一般被叫做*trait object* trait对象,我们将在第十七章接触到[3].
使用Box将数据放到heap上#
直接来看下例子
fn main() {
let b = Box::new(5);
println!("b = {}", b);
}Box也是prelude的,所以也可以直接用不需要手动导入.
我们把5这个原本应该存储到stack上的标量类型,现在存储到了heap上.
现在b是一个Box的类型,它是一个智能指针,指向存储堆内存里5所在的地址.
b这个box在超出作用域之后也是正常失效被释放,包括它指向的内存空间.
不过一般我们不会把单个值比如5这种标量类型放到heap上,正常情况下放stack上才是恰当的.
使用Box让递归类型能被使用#
这个场景就是场景1
递归这玩意儿通俗点的说就是套娃.
我来画张图

有点丑,但是应该挺好理解,一个元组的第二个数据的类型和这个元组的类型是一样的.
这种数据类型在编译阶段是没办法通过的,为什么呢?
因为理论上你这个套娃能套到内存溢出.
这个时候就轮到我们的Box出场了,box是可知大小的, 毕竟已经变成了一个指针.
这样处理其实只是把问题从编译阶段迁移到runtime阶段,绕过编译无法通过的问题而已.
(1, (2, (3, Nil)))这种套娃类型在其它函数式语言中一般叫做cons list.它有两种值,一个就是和它自己类型一样的,另一个就是Nil,表示到底了.
需要注意Nil和None不是同一个概念,None是无效不存在的意思,而Nil表示的是到底了的.
大部分情况下我们都不会在rust中使用这个类型,一般vector就够用了.
我们来看下rust中怎么使用它
enum List {
Cons(i32, List),
Nil,
}
fn main() {
use List::{Cons, Nil};
let list = Cons(0, (1, Cons(2, (3, Cons(4, Nil)))));
dbg!(list);
}我们定义了一个枚举List,它有两个变体,一个是Cons(i32, List),接收的数据一个元组类型;另一个则是Nil.
然后我们来运行下

直接就没法通过: 这是一个infinite size的类型.
在处理这个问题之前,我们来简单的认识下rust的编译器是怎么判断一个类型需要多少空间的.
先来看下例子
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {}这个Message的例子在之前我们接触过.
在编译阶段,编译器会把这个Message的枚举里的变体都过一遍,发现Quit压根不需要任何的空间,而Move则需要一个至少能容纳x, y两个数据的空间,其它也会看一遍需要多少.
由于一个枚举一次只会使用一个变体,比如Message::Move这样,所以rust的编译器就以这四个变体中所需要空间最多的那个为标准存储Message的类型数据.
然后它遇到了List这个套娃类型,这个时候就没办法推测了

现在我们知道了编译器是如何推断空间以及为什么推断不出来套娃类型需要的空间的原因了.
回到我们的代码中
这个时候就得借助Box把这个问题移到runtime来处理,runtime时根据实际情况有内存分配器分配空间.
前面说了Box是一个智能指针,所以它的空间肯定是可知的,毕竟到死都只是个指针(古惑仔不用脑,一辈子都是飞机~).
我们来改下我们的代码
#[derive(Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
use List::{Cons, Nil};
let list = Cons(0,Box::new(Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Cons(4, Box::new(Nil))))))))));
dbg!(list);
}

这回没报错了.
通过Box这么一包裹,这里就只剩下一个指针

需要注意的一点是:它在超出作用域的时候就会被释放,所以目前暂时不用担心内存泄漏的问题.
基于Deref trait把智能指针当作引用(reference)来对待#
实现Deref这个trait之后,我们就能自定义解引用了(dereference)的操作符*了代表的行为了.(这个不是相乘的意思).
同时实现Deref这个trait之后就能把智能指针当作普通的引用来使用了,当然,它依旧是一个智能指针,你也可以把它当作一个智能指针来使用.
我们先来看下*操作符
跟随指针读取数据#
我们先来看个例子
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, y);
}这样是会报错的,

为什么呢?
因为assert_eq!这个宏的right 是一个不可变引用,它的值是一段指向内存的地址,而left则是一个整数.
我们再改下代码
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
这样就正常了.
通过这个例子,我们可以得出*操作符是通知编译器去跟着地址找到内存里的数据然后读取数据,所以这里实际上就类似于
assert_eq!(5, 5);把Box当成引用来用#
我们来改下上面的代码
fn main() {
let x = 5;
let y = &x;
let z = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
assert_eq!(5, *z);
}可以看到y和z在这里的表现是一样的,去掉*也是有问题.
需要注意的是和引用不同的是Box是复制了一份x到heap上,然后再用指针指向这个heap上的数据,而不是直接创建一个引用。
定义一个自己的只能指针#
我们来自己搞一个类似Box的模拟下内部原理。
Box底层类似一个元组结构体,所以我们也搞一个,它还有一个new的关联函数。
所以我们先搞一个架子。
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}我们这个MyBox元组结构体接受一个数据,泛型T表示想要接收所有类型的值。
而元组数据是存储在stack上的,我们目前暂时没有办法去模拟放到堆内存上,所以将就下,毕竟我们的重点是这内部是怎么运作的。
现在我们可以使用这个MyBox了,你能将它类似引用的方式来使用,
不过编译是会报错的,因为我们并没有定义如何解引用。

然后为了可以使用这个解引用,我们要实现Deref这个trait。
实现Deref的要求是要实现deref这个method ,我们来写下。
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}由于Deref还是比较不常用的,所以没有prelude,我们需要手动引入。
type Target = T,前面的文章我们有说到过,这个是关联类型,表示私有返回值的类型。
deref这个方法返回的是我们之前接收的数据。
不过需要注意是&self.0,而不是self.0,所有权还得是自己保留。
现在我们可以直接使用*操作符来解引用了。
实际上*y操作在编译阶段会变成*(y.deref())这样,这也算是一种语法糖。
现在我们知道了Box内部大概是怎么样的了。
函数(Functions)和方法(Methods)中隐式的强制解引用(Deref Coercions)#
这个强制解引用(*Deref Coercions*)可能我翻译的不是很对,如果觉得不对可以点一下注释去官网看。
这个强制解引用可以让一个实现了Deref这个trait的类型的引用转换成另一个类型的引用。
有点拗口,举个栗子:&String可以变成&str,因为String类型实现了Deref这个trait。
在rust中,它对于函数和方法的参数来说是非常方便的,当然,前提是这个参数得实现Deref这个trait。
这个过程是自动的,当你传入的参数类型和要求的参数类型不一致的时候就会自动通过一系列对deref方法的调把参数类型转换成函数需要的类型。
fn main() {
hello(&String::from("dan"));
}
fn hello(name: &str) {
println!("Hello, {name}!");
}
可以看到完全没问题,这也就是为什么之前我们推荐&str作为参数类型的原因。
当然,我们也可以用我们的MyBox作为例子
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
hello(&MyBox::new(String::from("dan")));
}
fn hello(name: &str) {
println!("Hello, {name}!");
}
可以看到也没有问题,因为也实现了Deref这个trait。
编译器在发现类型不对的时候就会调用deref这个方法,然后获得到&String类型的数据,然后String自身又是实现了Deref这个trait的,所以它的deref方法也被调用了,然后就变成&str类型了。
这也就是为什么会用:a sequence也就是一些列这个词来描述的原因了。
以上的行为都是发生在编译阶段的,所以不用担心有runtime的损耗。
在可变类型中使用强制解引用#
我们前面看到的都是不可变的,如果用在可变的话就会报错
比如:
fn main() {
hello(&mut MyBox::new(String::from("dan")));
}
fn hello(name: &mut str) {
println!("Hello, {name}!");
}
这里编译器有提示我们实现DerefMut,而非Deref,我们来改下。
use std::ops::{Deref, DerefMut};
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<U> DerefMut for MyBox<U> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
hello(&mut MyBox::new(String::from("dan")));
}
fn hello(name: &mut str) {
println!("Hello, {name}!");
}

现在就没有报错了,类型也是正常自动转换。
目前我们还不知道如何改变&str的数据,所以我并没有改变hello方法中的name数据。
这一块可能有些乱,稍微梳理下:
我们这个MyBox实现的DerefMut的deref_mut方法返回的是我们存储的值,所以在发现这个数据类型对不上的时候它会变成&mut String,然后String它自己又实现了DerefMut这个trait,所以它也能变成&mut str类型。
以下几种场景是会触发强制解引用的
- 在
T实现了Deref的前提下,&T变成&U是没有问题的,比如上面的&String变成&str,就是因为String实现deref方法时的Target是&str。 - 同上,只不过
T和U都是mut的,并且T还实现了DerefMut,比如&mut String变成&mut str。 - 场景1,不过
T是mut的,这样也是可以转的,比如&mut String变成&str。
这里需要注意的是从mut变成immut是可以的,但是从immut变成mut就不行,原因还是因为借用规则和内存安全。
其实如果从头到尾只有这一个引用,那么它从不可变变成可变的也没啥问题。但是rust的借用规则无法保证这一点,所以直接封杀了事。
使用Drop Trait来清理运行中的代码#
这个trait其实我们之前就已经接触过了,一般一个变量超出作用域的时候就会调用这个trait的方法释放自己占用的资源。
既然是用来释放资源的,那就意味着所有的类型理论上都可以实现这个trait。
至于为啥要放在smart pointer这一章里讲,那是因为智能指针在实现的过程中大多数时候都是需要实现这个trait的,所以绕不过就干脆放这里讲了。
我们可以通过实现Drop Trait来自定义一个数据超出作用域的时候要做什么。
实现Drop这个trait的要求是实现drop这个方法。
我们来试下,Drop相对来说用的是比较多的,所以它被prelude了,我们不用手动引入。
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("my stuff"),
};
let d = CustomSmartPointer {
data: String::from("other stuff"),
};
println!("CustomSmartPointers created.");
}

可以看到是created的打印先触发,而drop里的打印是在c和d超出作用域之后才触发的。
不过你应该注意到了d的drop比c的触发早,因为变量是倒序的方式释放的。
有一点需要注意,那就是不用担心我们实现的drop覆盖了原本释放数据的drop,实际上我们的代码是合并到原本释放数据的代码里面去的。
使用std::mem::drop提前释放资源#
我们并不能禁止drop方法和调用drop方法来延迟或者提前释放资源。
延迟官方没有给api,但是提前有:std::mem::drop。
我们直接来看下例子
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created.");
drop(c);
println!("CustomSmartPointer dropped before the end of main.");
}

可以看到drop的打印提前了。
你可以基于Drop和这个std::mem::drop自己搞一个内存释放器,也不用担心会和系统自带的冲突。
引用计数智能指针:Rc#
绝大部分情况下我们的数据都只有一个拥有者。
还有极少部分情况存在多个拥有者,比如在图结构里,一个节点可能有多个点指向它,那么这个节点理论上就存在多个拥有者。
那么什么时候才能释放这个节点的数据呢?自然就是没有点指向它的时候,这个时候就轮到计数智能指针登场了。
Rc是reference counting的意思。
那么什么时候该用这个trait呢?
当你不确定某个heap上的数据在什么地方,在第几个引用结束的时候才清空,那么你就可以用这个Rc,等到没有引用的时候让它自动被释放。
使用Rc分享数据#
我们先来看个例子
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
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));
}上面的代码画成图类似下面这样

不过现在的代码是没办法通过编译的,为什么呢?因为a的所有权交给了b的元组第二个参数,c自然就拿不到了。

我们可以改为传入引用来避开这个问题
enum List<'a> {
Cons(i32, &'a Box<&'a List<'a>>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let _0 = Box::new(&Nil);
let _1 = Cons(10, &_0);
let _2 = Box::new(&_1);
let a = Cons(5, &_0);
let b = Cons(3, &Box::new(&a));
let c = Cons(4, &Box::new(&a));
}是不是很乱,看的头都炸了。
这个时候我们就可以用Rc来代替Box。
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}
直接就比生命周期看起来清晰很多。
Rc也是没有prelude,需要手动引入的。
Rc::clone并不是和之前用过的clone方法一样复制一份数据,而是增加这个引用的计数。
其实这里可以用a.clone(),但是rust开发团队是习惯是用Rc::clone(&a),两个都是一个意思。
clone一个Rc来增加它的计数#
我们改下上面的例子来看下这个计数的过程
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
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, Rc::clone(&a));
println!("count after creating c = {}", Rc::strong_count(&a));
}
println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}
可以用Rc::strong_count()来获取这个数据的引用计数。
可以看到当c超出作用域被释放之后计数就减少了1。
当main函数退出调用栈之后计数就会变成0,然后指向的内存的数据就会被释放。
Rc使得一个数据可以有多个“拥有者“,在某些时候是挺有用的,另外也不用担心会有泄漏的问题,为0的时候就自动释放了。
最后提一嘴,这个方法只能用于单线程。
Refcell和内部可变性模式(interior mutability pattern)#
*Interior mutability*是rust中的一种设计模式,它允许我们在数据存在不可变引用的前提下修改数据。
正常情况下是不能这么做的,因为rust的借用规则。
但是我们可以告知编译器这段代码由我们自己来判断是否正常没问题,这样虽然是unsafe的,但是确实能通过编译。
而Refcell就是一种基于内部可变性模式的类型。
使用Refcell强制借用规则在runtime时生效#
前面说了,我们通过这种方式把借用规则生效的时间迁移到了runtime,而非在编译阶段就判断。这也就意味着这段代码是unsafe的,一旦出了问题就是panic。
不过小意思,js中见得多了~
不过能放到编译的一定放编译阶段,runtime出问题的成本太高了,一不小心就是补上线。。。
来总结下Box、Rc、Refcell这三个分别是干啥的和区别,在什么时候用(突然莫名其妙的总结。。。估计是怕后面混了)
Rc,一个计数智能指针,允许一个数据存在多个"所有者"。Box和Refcell则只能用于单个“所有者”的场景。Box会将数据迁移到heap上并创建一个指针存放这个数据的地址。 它的借用规则和Rc一样都是在编译阶段就判断的,只是Box能绕开一些编译阶段无法处理的问题。而Refcell则是把借用规则的判断放到了runtime阶段来绕开编译会报错的问题。Refcell则是可以强制借用规则判断放到runtime阶段执行。你可以在Refcell里面改变数据,即使这个数据是不可变的,因为你已经通知编译器不要管这块代码了。
我们来看个场景
内部可变性(interior mutability):一个对不可变数据的可变借用#
先来看下平时我们可变借用一个不可变数据
fn main() {
let x = 5;
let y = &mut x;
}很明显是会报错的

原数据都不可变,自然不会有可变借用。
对于这个问题,我们一般用struct包裹。
比如这是个struct里的内部数据,它有一个method叫做set,用来修改自己的数据,然后还有一个get方法,返回这个数据的不可变引用。这样的场景应该挺常见的。
struct PriData {
data: i32,
}
impl PriData {
fn get(&self) -> i32 {
self.data
}
fn set(&mut self, new_val: i32) -> i32 {
self.data = new_val;
self.data
}
}
fn main() {
let x = PriData { data: 1 };
let y = x.get();
}这里只有一个改变数据的入口:set这个方法。
但是有时候就是不想写这么多东西,就是想要它是可变的,就是懒,怎么办呢?
这个时候就能用Refcell,把代码放到它里面就可以直接改变数据。
不过需要注意的是,它只是把问题移动到runtime而已,如果你的借用规则有问题,比如同时存在一个不可变借用和可变借用并且你这个可变引用改变数据是在可变引用声明之后使用之前,那大概率还是有问题会被panic掉。
至于要怎么改。。。等会再说
一个使用内部可变性模式的例子:Mock Objects#
Mock Object下面直接就叫模拟对象了。
有时候我们测试一块代码会用某个类型代替另一个类型,这么做是为了观察代码的反馈以及判断这块代码是否正常。
这种占位类型(placeholder type)就叫test double,这下面直接就叫测试替身了。
而模拟对象则是测试替身中的其中一种(白金之星~),它会记录测试过程中发生的事情。
rust中没有object这种类型,也没有在标准库中提供mock object。不过好在我们有struct,可以基于struct自己写一个~
我们来写一个模拟对象,一个小跟踪工具的demo,用来跟踪计算某个数和当前最大值的差值并给出提示信息。
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: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}这个小工具有两个method,一个是new,这个自然不必多说。还有一个set_value,它有两个参数,一个自然是self,不过注意它是mut的,然后第二个参数是需要计算的值。
目前这只是一个demo,实际上我们可以拓展为跟踪不同场景,然后发送邮件等。
使用这个跟踪工具库的开发者只需要传入实现了Messenger这个trait的参数,就可以使用了,不需要知道内部是如何实现的,当然,api名字还是得要知道的。
在实现send这个method的时候,我们可以自定义如何发送数据或者对提示信息做什么处理。
send方法接收两个参数,第一个自然是self,注意是不可变的,毕竟是暴露给库里的方法,还是不要给所有权最好。第二个参数则是我们的提示信息的切片。
为什么要单独抽出来这个Messenger的trait呢?
这么做是为了模拟的数据也可以实现这个Messenger从而调用这个库,不然就比较麻烦了,需要真实数据类型/真实场景才能开发。
我们目前唯一可以改变数据的入口是set_value,可以获取信息的入口是send这个method。
然后我们来写一个测试用例,测试返回的信息是否是我们想要的。
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: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[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);
}
}我们创建了一个MockMessenger的struct,它有一个私有数据sent_messages,是一个vector,用来存储 返回的提示信息。
但是这里有一个问题,那就是前面说的我们的send里的self是一个不可变的引用,而这里我们需要把提示信息存储到sent_message里,自然就需要self是mut的,这里就有问题了。

那么咋办呢?
这是你应该已经发现了,这个问题就是上一个小章节里提到的那个问题。它暴露了一个不可变self的方法,但是我们又想要是可变的。
这时就轮到这一章节的主角登场了。
我们来改下
#[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--
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}这里面一共调整了四处地方
- 在定义
struct的时候我们使用Refcell包裹Vec - 在
new方法创建实例的时候,我们使用Refcell包裹vec![] - 在调用
sent_messages这个Refcell实例的时候,我们调用了borrow_mut这个方法 - 在调用
assert_eq!比较长度的时候,我们调用了borrow这个方法
borrow_mut方法返回一个可变引用,源码咱就不看了,现在也看不懂。
那borrow自然就是返回一个不可变引用。
这样就能通过编译测试了

我们来看下它做了什么
使用Refcell在runtime跟踪借用#
我们之前创建引用的时候使用的方式是&或者&mut,现在我们多了一种方式。
通过Refcell实例调用borrow_mut和borrow。它们对于Refcell来说是相对安全的。
borrow返回的实际上引用是Ref类型,而borrow_mut自然就是RefMut,都是智能指针。
不过它们都实现了Deref这个trait,所以自然可以被当做引用。
Refcell会监听所有的Ref和RefMut,所以能知道当前有多少个智能指针是有效的。
每次调用borrow方法,它就会给Ref的智能指针计数加1,同理调用borrow_mut会给RefMut的计数加1。
而当这些引用超出作用域的时候就会减1。
不过上面都是发生在runtime期间的。
正如前面说过的,如果这个时候我们违背了借用规则,就会直接panic。
来看个例子
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: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[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) {
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_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.borrow().len(), 1);
}
}
其实还是之前的例子,不过我们在实现send的时候我们同时声明了两个可变引用,这时违背借用规则的。
你可以把这段代码放到main里面使用,然后cargo debug试下,编译是没有问题的。
但是当你cargo run或者cargo test的时候就会报错了。

Rc和Refcell搭配#
前面我们说了Rc是一个引用计数智能指针,然后上面一个小章节又说到我们在调用Refcell的borrow和borrow_mut的时候也会对引用计数。
并且同样在引用创建或者超出作用域的时候增减对应的数量。
目前我们已知的不同点:
Rc还是发生在编译阶段,而Refcell则是发生在runtime阶段。Rc只能返回不可变引用,而Refcell可以返回可变引用。Rc允许多个引用,而Refcell支持多个不可变引用或者单个可变引用(借用规则)
关于第二第三点,我们有一个想法,那就是结合Rc和Refcell,创建多个可变引用?
我们直接借用之前的那个cons llist吧,虽然有点绕。之前我们用Rc改写了之后,可以同时存在多个引用,在不需要使用生命周期的情况。
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}但是我们并不能去改数据,因为是不可变引用。
然后我们用Refcell来包裹,让它变成可变的
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
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 after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}
我们用Refcell包裹了5创建了一个Refcell的实例,然后又用Rc包裹了这个实例,这样就允许存在多个引用。
然后我们又调用Refcell的borrow_mut方法创建一个实现了RefMut的可变的引用,这样就存在多个可变引用了。
不用担心这样有问题,毕竟借用规则还是在的,只是跑到runtime去了。
不过记住目前哦们学的只有单线程的。
循环引用会导致内存泄漏#
内存泄漏这玩意儿难搞,没有GC的语言就不多说了,而有GC的语言也没办法保证能解决内存泄漏的问题。
rust虽然有自己独特的所有权规则,但是也是有可能遇到内存泄漏的问题的。
不过rust开发团队把这个当做是rust的一种安全的场景。。。。。。这里咱就不吐槽了,毕竟确实难解决。
创建循环引用#
还是之前cons list的例子
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}
fn main() {}
我们还写了一个tail方法,判断是否还没到底。
然后我们来调用下
fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
println!("a initial rc count = {}", Rc::strong_count(&a));
println!("a next item = {:?}", a.tail());
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
println!("a rc count after b creation = {}", Rc::strong_count(&a));
println!("b initial rc count = {}", Rc::strong_count(&b));
println!("b next item = {:?}", b.tail());
if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
}
println!("b rc count after changing a = {}", Rc::strong_count(&b));
println!("a rc count after changing a = {}", Rc::strong_count(&a));
// Uncomment the next line to see that we have a cycle;
// it will overflow the stack
// println!("a next item = {:?}", a.tail());
}重点是在*link.borrow_mut = Rc::clone(&b)
也就是说a又引用了b,而之前b引用了a。
这个时候就有问题了,a和b的引用不会变为0。

好像以前IE的垃圾回收器用的标记计数法也有这个问题(八股文走一波~)。

直接官网偷的图,这样方便理解。
我们还有一句println!注释掉了,如果注释回来就会一直打印直到溢出,因为这个时候已经循环引用了。

这场景正常情况下我们是不会遇到的,不过也不好说,一些递归场景还是有可能遇到的。
怎么说呢,自己多注意吧~
使用Weak代替Rc来避免循环引用#
相对于Rc::clone创建的强引用计数智能指针,Rc::downgrade会创建一个弱引用(weak reference),也就是Weak类型的智能指针,这个弱引用和强引用的区别在于分享所有权。
前面我说Rc是在创建多个引用,其实不是很对。因为这些引用实际上身份和原数据拥有者是一样的。
而通过clone出来的强引用可以通过strong_count来查看有多少“拥有者”。而弱引用不会被统计在strong_count里。它实际上不是这个数据的拥有者,也就是说它并不会影响Rc实例的清除。
而强引用被清空后也会让弱引用被清空,所以我们实际上可以用弱引用来回避循环引用的问题。
不过由于弱引用会因为强引用被清空而失效,所以开发者需要注意使用的范围。可以用过调用Weak实例的upgrade方法,这个方法会返回一个Option>,可以用这个来判断是否还有效。
让我们来试下。
创建一颗树:一个节点和它的子节点们#
其实套娃类型Cons List最常见的还是树结构,子节点类型和父节点类型是一样的。
我们直接来写一个
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
children: RefCell::new(vec![]),
});
let branch = Rc::new(Node {
value: 5,
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
}有一个叶节点和枝干节点,叶节点是枝干接点的子节点。
我们用Refcell包裹这个children,这样就能拓展了。
不过目前leaf和branch之间的关系只有开发者自己知道,而leaf并不知道branch是它的父节点。
我们来改下代码,让这个leaf节点知道它的父节点是这个branch。
不过在这之前,我们需要知道这个父节点得是什么类型的。如果按照之前的写法,这个叶节点就会引用这个枝干节点,那么就会形成前面说的循环引用问题。
这个时候就轮到Weak出场了。
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
} 父节点失效之后子节点自然得失效,而相反子节点失效后父节点不一定失效,用Weak太合适了。
现在我们的子节点引用了它的父节点,但是没有这个父节点的所有权。
ok,类型我们解决了,我们来使用下
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
} 
可以看到parent这个字段的数据不会展示所有。
可以看到并没有循环引用。
然后我们用Rc::strong_count和Rc::wak_count来看下计数
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
{
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!(
"branch strong = {}, weak = {}",
Rc::strong_count(&branch),
Rc::weak_count(&branch),
);
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}

这里把leaf的存放到父节点以及弱引用父节点都放到了作用域里面。
可以看到在超出作用域之后weak变成0了,同时strong也变回了1,并且这个时候的branch由于超出作用域也挂了。
所以我们使用Weak可以避免循环引用
总结#
不知道怎么说。。。。
这一章我们知道了什么是智能指针,它实现了Def和Drop两个trait。
然后我们还学了智能指针中三个比较常用的类型:
Box:能把数据迁移至heap上并创建一个指针。Rc: 能给一个数据创建多个“拥有者", 并记录它们Refcell:可以在runtime时再执行借用规则的判断,给不可变数据创建可变引用。
最后我们还知道了什么情况下会存在内存泄漏的问题以及如何使用Weak规避这个问题。
最后吐槽下。。。。这章内容太多了,想吐。早知道还是分几天来写算了。
但是下周两个需求,不太一定有时间摸鱼,烦。
另外可能有什么地方说的不对,请务必对照官网来看。。。
最后祝大家新的一年身体健康~
参考#
- ^rust-15-smart-pointers https://doc.rust-lang.org/book/ch15-00-smart-pointers.html#smart-pointers
- ^rust-15.1-use-Box-to-point-to-data-on-heap https://doc.rust-lang.org/book/ch15-01-box.html#using-boxt-to-point-to-data-on-the-heap
- ^rust-17-using-trait-object https://doc.rust-lang.org/book/ch17-02-trait-objects.html#using-trait-objects-that-allow-for-values-of-different-types
- ^rust-using-Box-to-store-data-on-the-heap https://doc.rust-lang.org/book/ch15-01-box.html#using-a-boxt-to-store-data-on-the-heap
- ^rust-enabling-recursive-with-boxes https://doc.rust-lang.org/book/ch15-01-box.html#enabling-recursive-types-with-boxes
- ^rust-15.2-treating-smart-pointer-like-reference https://doc.rust-lang.org/book/ch15-02-deref.html#treating-smart-pointers-like-regular-references-with-the-deref-trait
- ^rust-follow-pointer-to-the-value https://doc.rust-lang.org/book/ch15-02-deref.html#following-the-pointer-to-the-value
- ^rust-use-Box-like-reference https://doc.rust-lang.org/book/ch15-02-deref.html#using-boxt-like-a-reference
- ^rust-define-our-own-smart-pointer https://doc.rust-lang.org/book/ch15-02-deref.html#defining-our-own-smart-pointer
- ^rust-implicit-Deref-Coercions-with-functions-and-methods https://doc.rust-lang.org/book/ch15-02-deref.html#implicit-deref-coercions-with-functions-and-methods
- ^how-Deref-Coercions-interacts-with-mutability https://doc.rust-lang.org/book/ch15-02-deref.html#how-deref-coercion-interacts-with-mutability
- ^rust-running-code-cleanup-with-drop-trait https://doc.rust-lang.org/book/ch15-03-drop.html#running-code-on-cleanup-with-the-drop-trait
- ^rust-dropping-values-early-with-std::mem::drop https://doc.rust-lang.org/book/ch15-03-drop.html#dropping-a-value-early-with-stdmemdrop
- ^rust-Rc https://doc.rust-lang.org/book/ch15-04-rc.html#rct-the-reference-counted-smart-pointer
- ^rust-use-Rc-to-share-data https://doc.rust-lang.org/book/ch15-04-rc.html#using-rct-to-share-data
- ^rust-clone-Rc-increase-the-reference-count https://doc.rust-lang.org/book/ch15-04-rc.html#cloning-an-rct-increases-the-reference-count
- ^rust-Refcell https://doc.rust-lang.org/book/ch15-05-interior-mutability.html#refcellt-and-the-interior-mutability-pattern
- ^rust-enforcing-borrowing-rules-at-runtime-with-Refcell https://doc.rust-lang.org/book/ch15-05-interior-mutability.html#enforcing-borrowing-rules-at-runtime-with-refcellt
- ^rust-interior-mutability https://doc.rust-lang.org/book/ch15-05-interior-mutability.html#interior-mutability-a-mutable-borrow-to-an-immutable-value
- ^rust-keep-track-of-at-runtime-with-Refcell https://doc.rust-lang.org/book/ch15-05-interior-mutability.html#keeping-track-of-borrows-at-runtime-with-refcellt
- ^rust-different-between-Rc-and-Refcell https://doc.rust-lang.org/book/ch15-05-interior-mutability.html#having-multiple-owners-of-mutable-data-by-combining-rct-and-refcellt
- ^rust-reference-cycle-can-leak-memory https://doc.rust-lang.org/book/ch15-06-reference-cycles.html#reference-cycles-can-leak-memory
- ^rust-create-reference-cycle https://doc.rust-lang.org/book/ch15-06-reference-cycles.html#creating-a-reference-cycle
- ^rust-use-Weak-to-prevent-reference-cycle https://doc.rust-lang.org/book/ch15-06-reference-cycles.html#preventing-reference-cycles-turning-an-rct-into-a-weakt
- ^rust-create-a-tree https://doc.rust-lang.org/book/ch15-06-reference-cycles.html#creating-a-tree-data-structure-a-node-with-child-nodes
编辑于 2023-01-03 22:38・IP 属地广东
