Box<T>
将一个值放在堆上(而非栈上)。使用场景:
- 有一个编译时大小未知的类型,且要在需要确定大小的上下文中使用这个类型值。
- 有大量数据,且希望在确保数据不被拷贝的前提下转移所有权。
- 希望拥有一个值,且只关心它的类型是否实现了特定 trait 而不关心其具体类型。
let b = Box::new(5); // b: Box<i32>
println!("b = {b}"); // b = 5
递归类型:拥有同类型的值作为自身的一部分。
Rust 需要在编译时知道类型占用多少空间。递归类型的值理论上可以无限地嵌套下去,所以 Rust 不知道递归类型需要多少空间。通过在循环类型定义中插入 Box<T>
,可以创建递归类型。
cons list:一个嵌套列表,每一项都包含当前项的值以及下一项,下一项为 Nil
表示列表终止。
(1, (2, (3, Nil)))
尝试定义一个 cons list:
use crate::List::{Cons, Nil};
enum List {
Cons(i32, List),
Nil,
}
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
无法过编:
error[E0072]: recursive type `List` has infinite size
--> src\main.rs:3:1
|
3 | enum List {
| ^^^^^^^^^
4 | Cons(i32, List),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
4 | Cons(i32, Box<List>),
| ++++ +
Rust 如何确定非递归类型的空间大小?以 enum Message
为例:
enum Message {
Quit, // 无需空间
Move { x: i32, y: i32 }, // i32 * 2
Write(String), // String * 1
ChangeColor(i32, i32, i32), // i32 * 3
}
Message
所占空间为其最大成员所占的空间。
回到建议:
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
4 | Cons(i32, Box<List>),
| ++++ +
“indirection”的意思是不应储存一个值,而应储存一个指向值的智能指针 / 引用等。
Box<T>
是一个指针,它所需的空间为定值,因为指针的大小不会随其指向的数据的大小改变。修改后:
enum List {
Cons(i32, Box<List>),
Nil,
}
原本,List
的成员 Cons(i32, List)
包含 一个 List
,现在,Cons(i32, Box<List>)
变成了 指向 一个 List
。由于 Box<List>
指针的大小是固定值,编译器现在能够推导出 List
的大小为 i32
与 Box<List>
的大小之和。
智能指针也可以作为常规引用进行处理:
// 常规引用
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
// Box<T> 改写
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
根本上说,Box<T>
是包含一个元素的元组结构体,可以自定义智能指针:
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
无法过编,因为 Rust 不知道如何解引用 y: MyBox<{integer}>
。
为了使解引用运算符 *
有效,需要实现 Deref
trait。
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T; // 定义了用于此 trait 的关联类型
fn deref(&self) -> &Self::Target {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
*y
现在会被 Rust 展开为 *(y.deref())
。
TODO: 函数和方法的隐式 Deref 强制转换
TODO: Deref 强制转换如何与可变性交互
Rc<T>
有些特殊情况下,单个值可能有多个所有者(图中的一个结点可能被多条边指向)。
要启用多重所有权,需要显式使用 Rc<T>
,Rc
为 引用计数(reference counting),意味着通过记录一个值的引用数量检查这个值是否仍在被使用。如果某个值有零个引用,就代表没有任何有效引用并可以被清理。
想象客厅中的电视:第一个人进入客厅并打开电视,随后其他人也可以进入并观看或者选择离开,当最后一个人离开时他将关闭电视。
使用场景:要在堆上分配一些内存供程序的多个部分读取,且无法在编译时确定程序的哪一部分会最后使用它。(如果知道最后使用的部分,用一般的所有权规则即可)。
以上节的 cons list 为例,现在想要定义这样一个列表:
用一般的所有权写法,尝试让 b
和 c
都指向 a
:
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)); // error: use of moved value
无法编译,因为在定义 b
时已经移动 a
。
修改 List
的定义,用 Rc<T>
代替 Box<T>
,并改为克隆 2 次 a
所包含的 Rc<List>
。
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc; // Rc 不在 prelude 中
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a)); // 克隆语法, 可以改为 a.clone(),
let c = Cons(4, Rc::clone(&a)); // 但 Rc::clone 只增加计数, 高效
}
测试引用计数的变化规律:
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 dropping c = {}", Rc::strong_count(&a));
}
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after dropping c = 2
使用 Rc<T>
允许一个值有多个所有者,引用计数则确保:只要还存在任何所有者,其值就有效。
RefCell<T>
内部可变性:一种设计模式,允许当存在不可变引用时修改数据。它在数据结构中使用 unsafe
以模糊可变性与借用规则。
unsafe
:表明程序员在手动检查规则,而禁止编译器检查。
[回顾] 借用规则:
- 在任意给定时刻,只能拥有一个可变引用或任意数量的不可变引用 之一(而不是两者)。
- 引用必须总是有效的。
对于引用和 Box<T>
,借用规则的不可变性作用于 编译时,违反时无法过编;而对于 RefCell<T>
则作用于 运行时,违反时线程 panic
。
代码的一些属性不可能通过分析代码发现(参见 停机问题),这也是为什么编译器有时会出于保守而拒绝编译实际上正确的代码。这就是 RefCell<T>
的用武之地。
以下代码违反了借用规则,因为试图可变地借用一个不可变值:
fn main() {
let x = 5;
let y = &mut x;
}
但是在特定情况下,使一个值在其方法内部能够修改自身,而在其他代码中仍视为不可变,是很有用的(这样,只有在值的方法内部才能进行修改)。
TODO
智能指针对比:
Rc<T>
允许相同数据有多个所有者;Box<T>
和RefCell<T>
有单一所有者。Box<T>
允许在编译时执行不可变或可变借用检查;Rc<T>
仅允许在编译时执行不可变借用检查;RefCell<T>
允许在运行时执行不可变或可变借用检查。- 因为
RefCell<T>
允许在运行时执行可变借用检查,所以我们可以在即便RefCell<T>
自身是不可变的情况下修改其内部的值。
TODO: 引用循环和内存泄漏
👉 14 并发