Rust 核心概念解析:引用、借用与内部可变性
管理内存安全,特别是防止数据竞争和悬垂指针,是系统编程中的一个核心挑战。Rust 语言通过其所有权和借用检查系统,在编译阶段就为解决这些问题提供了强有力的保障。
本文聚焦于该系统的关键部分:引用。我们将详细解析共享引用 (&T) 与可变引用 (&mut T) 的工作原理与编译时规则,并进一步探讨"内部可变性"这一高级模式,它为特定场景提供了必要的灵活性。
引用及内部可变性
引用
- 通过引用,Rust 允许将值借用出去,但不放弃所有权
- 引用就是带有附加合约的指针
共享引用
- &T,就是可以共享的指针:
- 可同时存在任意数量的引用指向同一个值
- 每个共享的引用都实现了 Copy
- 背后的值不可变
- 编译器允许假定共享引用指向的值,在该引用存活期间是不会改变的
- 例如:一个共享引用的值在某函数内被多次读取,那编译器就有权让其只读取一次,然后重用读取的值。
可变引用
- &mut T
- 可变引用是独占的
- 编译器假定没有其它线程访问目标值(无论是通过共享引用还是可变引用)
rust
fn main() {
println!("Hello, world!");
}
fn noalias(input: &i32, output: &mut i32) {
if *input == 1 {
*output = 2;
}
if *input !=1 {
*output = 3;
}
}
- 可变引用只允许你修改引用所指向的内存地址
rust
fn main() {
let x = 42;
let mut y = &x; // y is of type &i32
let z = &mut y; // z is of type &mut &i32
}
拥有值 VS 拥有到值的可变引用
- 所有者需要对删除值(丢弃值)负责
- 警告:如果你移动了可变引用背后的值,则必须在其位置上留下另一个值。如果不这样做,所有者会认为他需要将其删除(丢弃),但其实却没有值可以删除了。
rust
fn main() {
let mut s = Box::new(42);
replace_with_84(&mut s);
}
fn replace_with_84(s: &mut Box<i32>) {
// this is not okay, as *s would be empty;
// let was = *s;
// but this is:
let was = std::mem::take(s);
// so is this:
*s = was;
// we can exchange values behind &mut:
let mut r = Box::new(84);
std::mem::swap(s, &mut r);
assert_ne!(*r, 84);
}
内部可变性
- 一些类型提供了内部可变性:
- 可通过共享引用修改值
- 这些类型通常依赖于额外的机制(如原子 CPU 指令)或不变量来提供安全的可变性,而不依赖于独占引用的语义
- 分为两类:
- 通过共享引用获得可变引用
- 通过共享引用可以替换值
- 通过共享引用获得可变引用:Mutex、RefCell
- 提供保障机制:针对任何提供了可变引用指向的值,同时只会存在一个可变引用(没有共享引用)
- 依赖于 UnsafeCell 类型,通过共享引用修改值的唯一正确方式
- 通过共享引用可以替换值:std::sync::atomic、std::cell::Cell
- 没有提供可变引用到内部值
- 提供就地操作值的方法
- 例:无法获得到 usize 或 i32 的直接引用,但是可以读取和替换值
Cell 类型
- 标准库
- 通过不变量实现的内部可变性
- 无法跨线程共享(内部值不会被并发的修改,即使通过共享引用发生修改)
- 不会提供到 Cell 内部的值的引用(所以可以一直移动它)
- 提供的方法:
- 对值整体替换
- 返回值的副本
总结
本文的核心在于 Rust 的引用系统,其安全性建立在两条基本原则之上:数据可以在多个共享引用 (&T) 之间只读共享,或者通过一个独占的可变引用 (&mut T) 进行修改。编译器的借用检查器严格实施这些规则,从而有效防止了数据竞争。
然而,为了在不牺牲安全的前提下提高灵活性,Rust 引入了"内部可变性"作为补充机制。像 Cell 等类型,将借用规则的检查从编译时推迟到运行时,允许在共享引用的访问范围内安全地修改数据。
因此,Rust 的内存安全模型是编译时静态检查和运行时动态检查的结合。理解这两种机制如何协同工作,是编写出高效且健壮的 Rust 代码的重要基础。