缺乏实践经验,感觉有些隔靴搔痒,抓不住重点,就这样吧。
在之前的学习中,了解了借用——对变量的引用,它是一种指针——指向(以及借用)其他地址上的值。智能指针是这样一种数据结构,它行为像指针,但包含额外的元信息。标准库中有各种各样的智能指针,包括但不限于已经学习过的和。智能指针和引用的一个重要区别在于,引用只借用数据,而很多时候智能指针拥有数据。同时,智能指针是一个 Rust 中常用的模式,许多第三方库中都能看到它的影子。
智能指针通常需要实现和, trait 让智能指针能够表现得像引用一样,而则自定义智能指针离开作用域时需进行的操作。
这里学习最常用的几种智能指针:——在堆上分配内存;——通过引用计数共享不可变所有权;——内部可变性,但在此之前要了解一下 Deref 和 deref 变换。
Deref 通过重载前缀运算符,让操作智能指针就像操作引用一样(不全是,比如模式匹配的时候)。下面定义一个行为类似(但其实还是分配在栈上)的智能指针并为其实现 Deref(注意 deref 方法返回的是引用):
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
fn main() {
let x: i32 = 5;
let y: MyBox<i32> = MyBox::new(x);
assert_eq!(*y, 5);
assert_eq!(x, 5);
}
这里的行为很诡异——如果说就是的实现的话,那的类型应该是啊?实际上,在遇到*x时,编译器会将其解释为*(x.deref())。为什么如此操作呢?首先能明确的是这是为了应付所有权系统。猜测是因为这里使用泛型T时,不知道T是否是Copy的(因此默认为不Copy的),如果在 deref 中直接返回类型 T 的话,就会把值移动出去了,这大多数时候不是我们想要的(况且,此时 self 也不能是引用,这让智能指针本身的所有权也丢掉了),为此只能委屈一下,在这里返回&T,然后在调用者处再次进行一次解引用,这就把是否移动的问题抛给调用者了。
deref coercion(解引用强制转换?)让我们能使用操作普通引用的代码去操作智能指针,其行为就像是在智能指针和引用类型(以及两个引用类型)之间建立了一个转换关系,Rust 会根据需要,自动地进行解引用直到满足约束。要利用到它,只需要实现 trait 即可。
最熟悉的 deref 转换是到,对于接受参数的函数,或者以为的方法,其可以传入;作为的实例可以看,其定义在中,接受,但的实例也可以调用该方法,其返回:
fn f(s: &str) {}
fn main() {
let s: String = "hello".to_owned();
f(&s);
dbg!(s.trim());
}
另外,上面编写的因为实现了,也可使用 deref 转换——可以将其传递给接受相应引用类型的函数,也可以直接调用其包装的值的类型上的方法:
fn f(s: &str) {}
fn main() {
let x: MyBox<String> = MyBox::new("hello".to_owned());
f(&x);
dbg!(x.trim());
}
Rust 实际上只能对引用类型进行真正的解引用操作。但本身也实现了,也就是说会自动解引用成,这使得下面的代码可行:
fn f(s: &str) {}
fn main() {
let a: &&&&&&str = &&&&&"ss";
let b: &str = &&&&&"ss";
f(a);
dbg!(a.trim());
}
最后,还有个,允许让 deref 变换结果是可变引用(当然,本身需要是可变引用)。
Box 是最直接的智能指针——它将数据分配到堆上而非栈上,在栈上只留一个指向堆中数据的指针。Box 没有性能开销,在下面的情况下使用:
递归类型,如链表和树,必须使用Box去包装引用自身的字段以保证编译时能确定大小
数据太大,希望移动所有权时减少拷贝数据消耗
希望持有特定 trait 的值,无关它的实际类型(即不知晓它的大小)
首先是递归类型,需要使用 Box 去定义递归类型,下面是链表定义的错误示范,它无法通过编译,因为一个的大小为的大小加上的大小,也就是说其大小是编译期不可知的,这样它在栈上分配空间的时候就不知道该分配多少:
enum List<T> {
Nil,
Cons(T, List<T>)
}
使用去包装对自身的引用以通过编译并在编译期(能够)明确的大小:
enum List<T> {
Nil,
Cons(T, Box<List<T>>)
}
Box 相较于其它的智能指针,它额外有一个功能——将它指向的值的所有权移动出来,并销毁自己,这是 Rust 语言本身的特性,从源码中无法看到:
let a: Box<String> = Box::new("hello".to_owned());
let b: String = *a;
dbg!(a); // use of moved value
在很多时候,所有权是清晰的,总是能明确某个值被谁持有,但也存在这样的情况,一个值它有多个所有者——如共享链表片段,如图结构。这种时候,应当在值没有任何所有者的时候才将它清除,Rc,即引用计数,即是为此目的而来——允许多个所有权,即共享数据给程序的不同部分,引用计数跟踪对值的引用数量以确定其是否在被使用。
考虑一个 Lisp 风格的链表,最初使用 Box 去定义它:
enum List {
Cons(i32, Box<List>), Nil
}

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));
这过不了编译——在定义 b 时,a 已经被移动进去了,定义 c 时就会报错使用了已经移动的值 a。倘若这合法,那 b 和 c 销毁的时候会导致 a 被销毁两次。谢谢你,所有权!解决方案是使用 Rc,Rc需要显式调用clone,但不用担心,它不会拷贝堆上的数据,只是一个指针的复制:
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::*;
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
// Rc 的所有操作使用关联函数(作为接口)也定义了一份,使避免和 Rc 指向的值的类型的操作混淆
// 根据约定,应当总是使用关联函数而非方法去调用Rc的方法
// Rc::clone 不会拷贝堆上的值,只是拷贝指针
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}
Rc仍旧可以返回可变借用,但它仍旧会保证同时只有一个可变借用。
Rc是不线程安全的,若要在多线程中共享数据,使用Arc。
RefCell只允许单个所有者,但它和Box不同——RefCell在运行时检查借用规则。Rust提供RefCell是为了支持一些静态分析过不去但实际上有意义的情景,此时维护借用规则的职责交由程序员。同样是因为借用规则在运行时检查,程序员可以得到内部可变性(interior mutability,rust的一种设计模式)——可以修改RefCell中的值,即使只持有着不可变借用或值,如下面的例子,即使a没有标注mut,仍旧可以从中获得可变借用。
let a: RefCell<Vec<i32>> = RefCell::new(vec![1]);
let mut ref_mut: std::cell::RefMut<'_, Vec<i32>> = a.borrow_mut();
ref_mut.push(1);
// drop(ref_mut); // 取消注释此行使不 panic
let r: std::cell::Ref<'_, Vec<i32>> = a.borrow(); // panic
使用RefCell的一个常见情景是测试时定义mock object,在不可变上下文中修改自身来便于测试。
允许共享不可变数据的所有权,而表现得像是不可变数据……这就是说,允许共享可变数据的所有权。
RefCell也不是线程安全的,它的线程安全版本是(!??)。
在RefCell中包含Rc,允许制造出循环引用:
#[derive(Debug)]
struct Cycle(RefCell<Option<Rc<Cycle>>>);
fn main() {
let cycle = Rc::new(Cycle(RefCell::new(None)));
*cycle.0.borrow_mut() = Some(Rc::clone(&cycle));
dbg!(cycle); // 根本停不下来
}
只使用Rc是造不出循环引用的——在引用自己前,必须先clone,而clone后就无法可变借用自己了;先可变借用自己再clone又违反了所有权。
一般来说循环引用是逻辑bug,但因为特殊需求必须要使用循环引用的话,考虑把特定的Rc替换成Weak——重新组织数据结构,区分所有权关系和非所有权关系。