了解过 Deref 和 Drop 两个基本特征后,智能指针神秘面纱的一角已经显现出来。下面会介绍常见的智能指针。
Box<T>Box<T> 只进行简单的堆内存分配,也就是将数据存放在堆上,同时在栈上保存一个指向堆的指针。它拥有其分配内存的所有权,并在离开作用域时释放其中内容。
let val: u8 = 5;
// 通过创建 `Box` 将值从栈移到堆
let boxed: Box<u8> = Box::new(val);
// 通过解引用将值从 `Box` 移回栈
let val: u8 = *boxed;
Box<T> 对于创建递归的数据结构很有用。如果不用 Box 包裹数据会无法经过编译检查,因为编译器无法推定整个数据结构的内存分配大小。
// 此枚举是递归的数据结构
// 如果第一个变体定义为 `Cons(T, List<T>)`
// 会无法通过编译
enum List<T> {
Cons(T, Box<List<T>>),
Nil,
}
let list: List<i32> = List::Cons(
1,
Box::new(List::Cons(
2,
Box::new(List::Nil)
))
);
由于 Box 是简单的封装,除了将值存储在堆上外,并没有其它性能上的损耗,代价是也没什么别的功能。
Box 和普通的数据在所有权上表现差不多:一个 Box 只能有一个所有者。
由所有权与借用规则可以推知:无法可变地借用一个不可变值。
let x = 5;
let y = &mut x;
// 编译失败,提示:
// cannot borrow `x` as mutable, as it is not declared as mutable
然而,特定情况下令一个值在其方法内部能够修改自身,而在其他代码中仍视为不可变,是很有用的(这样值方法的外部代码就不能修改其值了)。为此我们提出内部可变性的概念。
内部可变性(Interior mutability)是 Rust 中的一种设计模式,它允许你即使在有不可变引用时也可以改变数据,简言之就是在不可变值内部改变值。这通常是借用规则所不允许的。
Cell<T>、RefCell<T> 和 OnceCell<T> 类型的值可以通过共享引用(即 &T 类型)进行修改,而大多数 Rust 类型只能通过可变引用(&mut T)进行修改。因此我们说这些容器类型提供了内部可变性(通过 &T 可变)。
要注意,
Cell<T>、RefCell<T>和OnceCell<T>类型都只适合单线程环境,它们均未实现Sync。如果想在多线程环境中实现内部可变性,Mutex<T>、RwLock<T>、OnceLock<T>或原子类型才是正确的选择。
Cell<T>Cell<T> 通过将值移入和移出容器来实现内部可变性。通过把数据包装在 Cell 里,即使只有不可变引用(&Cell<T>)你也能修改内部的值。代价是你永远无法拿到内部值的引用。
换言之,一旦把数据放入 Cell,对其进行修改轻而易举(set 方法就行),但是想访问它却变得很困难,除非:
replace 等方法),这会将它返回,因为此时发生了值被移出容器的操作;into_inner 方法)来把值取出来;Copy,你可以通过 get 方法来获得它的拷贝,但这并不是容器内部值本身。下面是使用 Cell 在不可变结构体内部实现可变性的例子:
use std::cell::Cell;
struct SomeStruct {
regular_field: u8,
special_field: Cell<u8>,
}
let my_struct = SomeStruct {
regular_field: 0,
special_field: Cell::new(1),
};
let new_value = 100;
my_struct.special_field.set(new_value);
// 如果写成
// my_struct.regular_field = new_value;
// 编译就会报错
// 因为 `my_struct` 是不可变的
// 但其内部的 `special_field` 字段是 `Cell`
// 因此在内部可变性的加持下
// 我们能修改 `special_field`
assert_eq!(my_struct.special_field.get(), new_value);
Cell<T> 通常用于复制或移动值时开销不大的简单类型(例如数字)。
RefCell<T>Rust 的所有权与借用规则强有力地保障了程序运行时的内存安全。绝大多数情况下,编译器会在编译阶段就应用这些规则对代码进行分析检查:符合规则就放行;违反规则就抛出错误拒绝编译。这样既捕获了那些内存安全的隐患,又能让程序运行时的性能得到最大发挥。
但有些时候,这种编译时的行为会成为程序开发的阻碍。静态分析——正如 Rust 编译器所做的——天生保守,但代码的一些属性是不可能通过分析静态代码发现的。
很多情况下,你确信代码遵守了借用规则,而编译器却不能理解和确定。此时我们选择将对某些代码的借用检查从编译期延后至运行时。这相当于告诉编译器:“我为我的代码担保运行时不会出现内存安全问题,所以你也应该放我们编译通过。”
OnceCell<T>某种程度上是Cell和RefCell的混合体
RefCell<T>并不是绕开了借用规则,借用规则始终是生效的,只不过对规则的检查从编译期延后至运行时了。如果违反了这些规则,程序依然会在运行时崩溃而不是出现编译错误。
RefCell 的一种常见用法是与 Rc 结合使用。
Rc<T>Rust 所有权机制要求一个值只能有一个所有者,在大多数情况下这都没有问题,但是考虑以下情况:
以上场景不是很常见,但是一旦遇到就非常棘手,为解决此类问题,Rust 在所有权机制之外又引入了额外的措施来简化相应的实现:通过引用计数的方式,允许一个数据资源在同一时刻拥有多个所有者。
在单线程环境下,使用引用计数的智能指针是 Rc。
Rc<T> 类型提供了对 T 类型值的共享所有权,该值在堆上分配。对 Rc 调用 clone 会生成一个新的指针,指向堆上的同一分配区域。当指向某分配区域的最后一个 Rc 指针被销毁时,该分配区域中存储的值(通常称为“内部值”)也会被丢弃。
在 Rust 中,共享引用默认禁止修改,Rc 也不例外:通常无法获得指向 Rc 内部值的可变引用。如果需要可变性,可以在 Rc 内部放置一个 Cell 或 RefCell。
use std::cell::{RefCell, RefMut};
use std::collections::HashMap;
use std::rc::Rc;
fn main() {
let shared_map: Rc<RefCell<_>> = Rc::new(RefCell::new(HashMap::new()));
// 创建一个代码块来限制借用的作用域
{
let mut map: RefMut<'_, _> = shared_map.borrow_mut();
map.insert("africa", 92388);
map.insert("kyoto", 11837);
map.insert("piccadilly", 11826);
map.insert("marbles", 38);
}
// Note that if we had not let the previous borrow of the cache fall out
// of scope then the subsequent borrow would cause a dynamic thread panic.
// This is the major hazard of using `RefCell`.
let total: i32 = shared_map.borrow().values().sum();
println!("{total}");
}
Rc 使用非原子引用计数。这意味着开销非常低,但 Rc 无法在线程间传递,因此 Rc 并未实现 Send trait。这样,Rust 编译器会在编译时检查以确保你不会在线程间传递 Rc。如果你需要多线程环境下的原子引用计数,请使用 sync::Arc。
downgrade 方法可用于创建非所有权的 Weak 指针。Weak 指针可以升级为 Rc,但如果分配区域中存储的值已经被丢弃,则升级会返回 None。换句话说,Weak 指针不会保持分配区域内部值的存活状态,但它们会保持分配区域(内部值的后备存储)的存活。
Rc 指针之间的循环将永远不会被释放。因此,Weak 被用于打破循环。例如,一棵树中,父节点到子节点可以使用强 Rc 指针,而子节点回到父节点则可以使用 Weak 指针。
Rc<T> 会自动解引用为 T(通过 Deref trait),因此你可以在 Rc<T> 类型的值上直接调用 T 的方法。为了避免与 T 的方法名称冲突,Rc<T> 自身的方法都是关联函数,需要使用完全限定语法来调用:
use std::rc::Rc;
let my_rc = Rc::new(());
let my_weak = Rc::downgrade(&my_rc);
Rc<T> 对诸如 Clone 等 trait 的实现也可以使用完全限定语法来调用。有些人偏好使用完全限定语法,而另一些人则偏好使用方法调用语法。
use std::rc::Rc;
let rc = Rc::new(());
// 方法调用语法
let rc2 = rc.clone();
// 完全限定语法
let rc3 = Rc::clone(&rc);
Weak<T> 不会自动解引用为 T,因为内部值可能已经被丢弃。
克隆引用
通过为 Rc<T> 和 Weak<T> 实现的 Clone trait,可以创建指向同一分配区域的新引用。
use std::rc::Rc;
let foo = Rc::new(vec![1.0, 2.0, 3.0]);
// 以下两种语法是等价的。
let a = foo.clone();
let b = Rc::clone(&foo);
// a 和 b 都指向与 foo 相同的内存位置。
Rc::clone(&from) 这种语法是最符合惯用写法的,因为它能更明确地表达代码的意图。在上面的例子中,这种语法能更清晰地表明代码是在创建一个新的引用,而不是复制 foo 的全部内容。
示例
考虑这样一个场景:一组 Gadget(小工具)归属于某个 Owner(所有者)。我们希望 Gadget 能够指向其 Owner。使用唯一所有权无法实现这一点,因为多个 Gadget 可能属于同一个 Owner。Rc 允许我们在多个 Gadget 之间共享一个 Owner,并且只要还有 Gadget 指向它,Owner 就会保持分配状态。
use std::rc::Rc;
struct Owner {
name: String,
// ...其他字段
}
struct Gadget {
id: i32,
owner: Rc<Owner>,
// ...其他字段
}
fn main() {
// 创建一个引用计数的 `Owner`。
let gadget_owner: Rc<Owner> = Rc::new(
Owner {
name: "Gadget Man".to_string(),
}
);
// 创建属于 `gadget_owner` 的 `Gadget`。
// 克隆 `Rc<Owner>` 会给我们一个新的指针,指向同一个 `Owner` 分配区域,
// 同时增加引用计数。
let gadget1 = Gadget {
id: 1,
owner: Rc::clone(&gadget_owner),
};
let gadget2 = Gadget {
id: 2,
owner: Rc::clone(&gadget_owner),
};
// 丢弃我们的局部变量 `gadget_owner`。
drop(gadget_owner);
// 尽管丢弃了 `gadget_owner`,我们仍然能够打印出 `Gadget` 所属 `Owner` 的名字。
// 这是因为我们只丢弃了一个 `Rc<Owner>`,而不是它指向的 `Owner` 本身。
// 只要还有其他 `Rc<Owner>` 指向同一个 `Owner` 分配区域,它就会继续保持存活。
// 字段投影 `gadget1.owner.name` 之所以有效,是因为 `Rc<Owner>` 会自动解引用为 `Owner`。
println!("Gadget {} owned by {}", gadget1.id, gadget1.owner.name);
println!("Gadget {} owned by {}", gadget2.id, gadget2.owner.name);
// 在函数末尾,`gadget1` 和 `gadget2` 被销毁,随之销毁的是指向我们 `Owner` 的最后几个计数引用。
// 此时,Gadget Man 也被销毁。
}
如果我们的需求发生变化,还需要能够从 Owner 遍历到 Gadget,我们就会遇到问题。从 Owner 到 Gadget 的 Rc 指针会引入一个循环。这意味着它们的引用计数永远不会变为 0,分配区域将永远不会被销毁:这就造成了内存泄漏。为了解决这个问题,我们可以使用 Weak 指针。
实际上,Rust 使得产生这种循环变得有些困难。要得到两个相互指向的值,其中一个必须是可变的。这很困难,因为 Rc 通过只提供对其包装值的共享引用来保证内存安全,而这些共享引用不允许直接修改。我们需要将希望修改的那部分值包裹在 RefCell 中,RefCell 提供了内部可变性:一种通过共享引用实现可变性的方法。RefCell 在运行时强制执行 Rust 的借用规则。
use std::rc::Rc;
use std::rc::Weak;
use std::cell::RefCell;
struct Owner {
name: String,
gadgets: RefCell<Vec<Weak<Gadget>>>,
// ...其他字段
}
struct Gadget {
id: i32,
owner: Rc<Owner>,
// ...其他字段
}
fn main() {
// 创建一个引用计数的 `Owner`。注意,我们将 `Owner` 的 `Gadget` 向量放在了 `RefCell` 中,
// 这样我们就可以通过共享引用对其进行修改。
let gadget_owner: Rc<Owner> = Rc::new(
Owner {
name: "Gadget Man".to_string(),
gadgets: RefCell::new(vec![]),
}
);
// 像之前一样,创建属于 `gadget_owner` 的 `Gadget`。
let gadget1 = Rc::new(
Gadget {
id: 1,
owner: Rc::clone(&gadget_owner),
}
);
let gadget2 = Rc::new(
Gadget {
id: 2,
owner: Rc::clone(&gadget_owner),
}
);
// 将 `Gadget` 添加到它们的 `Owner` 中。
{
let mut gadgets = gadget_owner.gadgets.borrow_mut();
gadgets.push(Rc::downgrade(&gadget1));
gadgets.push(Rc::downgrade(&gadget2));
// `RefCell` 的动态借用在此结束。
}
// 遍历我们的 `Gadget`,打印其详细信息。
for gadget_weak in gadget_owner.gadgets.borrow().iter() {
// `gadget_weak` 是一个 `Weak<Gadget>`。由于 `Weak` 指针不能保证分配区域仍然存在,
// 我们需要调用 `upgrade`,它会返回一个 `Option<Rc<Gadget>>`。
//
// 在这个例子中,我们知道分配区域仍然存在,所以我们直接对 `Option` 调用 `unwrap`。
// 在更复杂的程序中,你可能需要对 `None` 的结果进行优雅的错误处理。
let gadget = gadget_weak.upgrade().unwrap();
println!("Gadget {} owned by {}", gadget.id, gadget.owner.name);
}
// 在函数末尾,`gadget_owner`、`gadget1` 和 `gadget2` 被销毁。
// 此时不再有指向这些 gadget 的强 (`Rc`) 指针,因此它们被销毁。
// 这也使得 Gadget Man 的引用计数归零,因此他也会被销毁。
}