内部可变性(Interior Mutability)

让我们花点时间分析一下Sendersend方法的签名:

#![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(); // 引发恐慌!存在活跃的不可变借用。
}