内部可变性(Interior Mutability)
让我们花点时间分析一下Sender
的send
方法的签名:
#![allow(unused)] fn main() { impl<T> Sender<T> { pub fn send(&self, t: T) -> Result<(), SendError<T>> { // [...] } } }
send
接受&self
作为其参数。但它显然造成了状态的变更:它向通道中添加了一条新的消息。更有趣的是,Sender
是可以克隆的:我们可以有多份Sender
实例试图同时从不同线程修改通道的状态。
这正是我们构建这种客户端-服务器架构所利用的关键特性。但为什么这能行得通呢?难道它不违反了Rust关于借用的规则吗?我们是如何通过一个不可变的引用来进行修改的呢?
共享而非不可变的引用
当我们引入借用检查器时,我们提到了Rust中可以拥有的两种类型的引用:
- 不可变引用(
&T
) - 可变引用(
&mut T
)
其实更准确的说法应该是:
- 共享引用(
&T
) - 排他引用(
&mut T
)
不可变/可变是一种适用于绝大多数情况的心智模型,并且它是初学Rust时非常好的入门概念。但正如你刚刚看到的,这并不是全部:&T
实际上并不保证它指向的数据是不可变的。不过别担心,Rust仍然遵守着它的承诺。只是这些术语比最初看起来的要微妙一些。
UnsafeCell
每当一个类型允许你通过共享引用修改数据时,你就是在处理内部可变性。
默认情况下,Rust编译器假设共享引用是不可变的。它基于这一假设优化你的代码。编译器可以重新排序操作,缓存值,并做各种魔法来加速你的代码。
你可以通过将数据包裹在UnsafeCell
中告诉编译器:“不,这个共享引用实际上是可变的”。每当你看到一个允许内部可变性的类型时,你可以肯定UnsafeCell
直接或间接地参与其中。使用UnsafeCell
、原始指针和unsafe
代码,你可以通过共享引用来修改数据。
但要明确的是:UnsafeCell
并不是一根可以让你无视借用检查器的魔法棒!unsafe
代码仍然受到Rust关于借用和别名规则的约束。它是一个(高级)工具,你可以利用它来构建那些安全性不能直接用Rust类型系统表达的安全抽象。每当你使用unsafe
关键字时,你都在告诉编译器:“我知道我在做什么,我不会违反你的不变量,请相信我。”
每次你调用一个unsafe
函数时,都会有关于其安全先决条件的文档说明:在什么情况下执行其unsafe
块是安全的。你可以在Rust标准库的文档中找到关于UnsafeCell
的。
在这个课程中,我们不会直接使用UnsafeCell
,也不会编写unsafe
代码。但了解它的存在、它为何存在以及它与你在Rust中每天使用的类型之间的关系是很重要的。
关键示例
让我们浏览几个利用内部可变性的标准库类型。这些是你在Rust代码中会经常遇到的类型,尤其是当你深入了解你所使用的某些库的内部机制时。
引用计数
Rc
是一个引用计数指针。它包裹一个值并跟踪对该值存在的引用数量。当最后一个引用被丢弃时,该值会被释放。被Rc
包裹的值是不可变的:你只能获得对其的共享引用。
#![allow(unused)] fn main() { use std::rc::Rc; let a: Rc<String> = Rc::new("我的字符串".to_string()); // 对字符串数据只有一个引用。 assert_eq!(Rc::strong_count(&a), 1); // 当我们调用`clone`时,字符串数据并没有被复制! // 相反,`Rc`的引用计数被递增。 let b = Rc::clone(&a); assert_eq!(Rc::strong_count(&a), 2); assert_eq!(Rc::strong_count(&b), 2); // ^ `a`和`b`都指向相同的字符串数据 // 并共享同一个引用计数器。 }
Rc
内部使用UnsafeCell
来允许共享引用增加和减少引用计数。
RefCell
RefCell
是Rust中内部可变性的最常见例子之一。
它允许你即使只有一个对RefCell
本身的不可变引用,也能修改RefCell
包裹的值。
这是通过运行时借用检查实现的。RefCell
在运行时跟踪它包含的值的引用数量(和类型)。如果你尝试在已有不可变借用的情况下可变地借用值,程序将panic,确保Rust的借用规则始终得到执行。
#![allow(unused)] fn main() { use std::cell::RefCell; let x = RefCell::new(42); let y = x.borrow(); // 不可变借用 let z = x.borrow_mut(); // 引发恐慌!存在活跃的不可变借用。 }