Rust 智能指针
所有权和借用规则能应付大多数情况,但有几种真实场景没法简单地套进“只有一个所有者”:值的大小要到运行时才知道;数据确实需要不止一个所有者;或者你需要通过共享引用去修改一个值。智能指针(smart pointer)就是 Rust 针对这些情况给出的答案。
Box<T>:把值放到堆上
Section titled “Box<T>:把值放到堆上”默认情况下,值存放在栈上,编译器需要提前知道它们的大小。Box<T> 把值移动到堆上,交给你一个指向它、大小固定的指针。当某个类型的大小在编译时无法确定时,它就派上用场了,最常见的就是递归类型:
fn main() { let boxed = Box::new(5); println!("{}", *boxed); // 用 * 取出里面的值}输出:
5一个必须用 Box 的经典例子:包含自身的类型,比如链表节点:
enum List { Cons(i32, Box<List>), Nil,}
fn main() { let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
match list { List::Cons(value, _) => println!("第一个值:{value}"), List::Nil => println!("空列表"), }}输出:
第一个值:1如果没有 Box,Cons(i32, List) 需要无限大的空间来存储:一个 List 里面装着一个 List,里面又装着一个 List,没完没了。而 Box<List> 是一个大小固定的指针,这样编译器就能算出它需要多少空间了。
Rc<T>:多个所有者
Section titled “Rc<T>:多个所有者”所有权规则说,一个值只有一个所有者。大多数时候这样正合适,但有时你确实需要让程序的多个部分共享同一份数据。Rc<T>(reference counted,引用计数)就允许这样做,它会记录当前有多少个所有者:
use std::rc::Rc;
fn main() { let a = Rc::new(String::from("共享数据")); let b = Rc::clone(&a); // 不是深拷贝,只是多了一个所有者 let c = Rc::clone(&a);
println!("{a} {b} {c}"); println!("所有者数量:{}", Rc::strong_count(&a));}输出:
共享数据 共享数据 共享数据所有者数量:3Rc::clone 不会复制底层的数据,它只是把计数器加 1,再交给你一个指向同一个值的引用。只有当所有的所有者都离开作用域之后,数据才会被清理掉。
RefCell<T>:通过共享引用修改值
Section titled “RefCell<T>:通过共享引用修改值”通常,借用规则是在编译时检查的:要么有多个只读引用,要么只有一个可变引用,两者不能同时存在。RefCell<T> 把这项检查挪到了运行时,这样即使你手里拿的看起来是一个共享的、不带 mut 的引用,也能修改里面的值:
use std::cell::RefCell;
fn main() { let value = RefCell::new(5);
*value.borrow_mut() += 1;
println!("{}", value.borrow());}输出:
6RefCell 最常和 Rc 搭配使用:Rc<RefCell<T>> 给你一个有多个所有者、而且仍然可以修改的值,这是单独一个 Rc<T> 做不到的。
什么时候真正需要它们
Section titled “什么时候真正需要它们”别一上来就用智能指针。先用普通的所有权和借用,只有当编译器明确告诉你行不通时再考虑它们:递归类型(Box)、确实需要多个所有者的值(Rc),或者借用规则不允许的、通过共享引用进行的修改(RefCell)。
1. 求链表所有值的和
Section titled “1. 求链表所有值的和”使用 Box 一节里的 List 枚举。构建一个包含 1、2、3 的链表,并写一个函数 fn sum(list: &List) -> i32,把所有值加起来。提示:对链表做 match。对于 Cons(value, rest),结果是 value 加上 rest 的和;对于 Nil,结果是 0。
查看答案
enum List { Cons(i32, Box<List>), Nil,}
fn sum(list: &List) -> i32 { match list { List::Cons(value, rest) => value + sum(rest), List::Nil => 0, }}
fn main() { let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil)))))); println!("总和:{}", sum(&list));}输出:
总和:6这个函数会对链表剩下的部分调用自身,直到遇到 Nil 为止。注意,在需要 &List 的地方,你可以直接传入 rest(它是 &Box<List>),Rust 会自动帮你透过 Box 拿到里面的值。
2. 共享的购物清单
Section titled “2. 共享的购物清单”创建一份购物清单(一个 Vec<String>),让 alice 和 bob 两个人共同拥有,而且两个人都能往里面添加东西。每人添加一项,然后打印清单和所有者的数量。提示:把 Vec 包进 Rc<RefCell<...>>,给每个人一个 Rc::clone,再调用 .borrow_mut().push(...) 添加。
查看答案
use std::cell::RefCell;use std::rc::Rc;
fn main() { let list = Rc::new(RefCell::new(Vec::new())); let alice = Rc::clone(&list); let bob = Rc::clone(&list);
alice.borrow_mut().push(String::from("牛奶")); bob.borrow_mut().push(String::from("面包"));
println!("{:?}", list.borrow()); println!("所有者数量:{}", Rc::strong_count(&list));}输出:
["牛奶", "面包"]所有者数量:3这里一共有三个所有者:list、alice 和 bob。三者都指向同一个 Vec,所以通过 alice 添加的东西,通过 bob 和 list 也能看到。这些变量都不需要 mut,因为修改由 RefCell 负责处理。
Box<T>把值存放在堆上,用一个大小固定的指针指向它,递归类型离不开它。Rc<T>通过引用计数,让单线程代码中的同一个值可以有多个所有者;跨线程时用Arc<T>。RefCell<T>在运行时而不是编译时检查借用规则,允许通过共享引用修改值,但违反规则时会 panic。- 只有当普通的所有权和借用不适合你的情况时,才去用它们。
小测验
1. 为什么像链表节点这样的递归类型需要 Box<T>?
2. Rc::clone(&a) 实际上做了什么?
3. RefCell<T> 在什么时候检查借用规则?
4. Rc<RefCell<T>> 能做到而单独的 Rc<T> 做不到的是什么?
得分: 0 / 4
- 使用
Box<T>指向堆上的数据 — Rust 程序设计语言:Box的完整介绍,包括本页用到的 cons list 例子。 Rc<T>引用计数智能指针 — Rust 程序设计语言:一步步讲解共享所有权,观察计数怎样增加和减少。RefCell<T>和内部可变性模式 — Rust 程序设计语言:什么时候、为什么要把借用检查挪到运行时,附一个贴近实际的例子。std::cell— 标准库文档(英文):讲解Cell和RefCell,以及各自适合什么场景。- Rc — 通过例子学 Rust:一个简短的可运行示例,随着所有者的增减打印引用计数。