Rust unsafe 详解
没有悬垂引用、没有数据竞争、没有越界访问:这门课讲过的每一项保证,都是编译器替你把关的。unsafe 是你对编译器说的一句话:“让我做一件你没法证明安全的事,责任由我来承担。”
unsafe 能做什么,不能做什么
Section titled “unsafe 能做什么,不能做什么”很多人以为 unsafe 会把 Rust 的安全检查全部关掉。其实不会。在 unsafe 块里,你只是多了 5 件平时不允许做的事:
- 解引用裸指针(raw pointer)
- 调用
unsafe函数 - 访问或修改可变的
static变量 - 实现
unsafetrait - 访问
union的字段
除此之外的一切,比如借用规则、移动语义、类型检查,在 unsafe 代码里照样完全有效。它是一把只用于 5 种操作的手术刀,而不是安全检查的总开关。
Rust 的普通引用(& 和 &mut)总是指向有效且正确对齐的数据,这是编译器保证的一部分。裸指针(*const T 或 *mut T)没有这个保证,所以读取它需要 unsafe:
fn main() { let value = 5; let pointer = &value as *const i32;
unsafe { println!("{}", *pointer); }}输出:
5在安全代码里就可以创建裸指针,只有解引用它(读取它指向的值)才需要 unsafe,因为编译器已经没法确认这个指针是否有效了。
调用 unsafe 函数
Section titled “调用 unsafe 函数”有些函数本身就标记为 unsafe,因为它们有编译器检查不了的前提条件。调用这样的函数,就等于你承诺:这些前提条件我已经亲自确认过了。
unsafe fn dangerous() { println!("这个函数相信你已经确认过它的前提条件");}
fn main() { unsafe { dangerous(); }}输出:
这个函数相信你已经确认过它的前提条件把 unsafe 包在安全函数里
Section titled “把 unsafe 包在安全函数里”标准库里有一些真正的 unsafe 函数可以调用,slice 的 get_unchecked 就是一个。它和 numbers[i] 的作用一样,只是跳过了边界检查。如果 i 越界,它不会 panic,而是读取那块内存里碰巧存着的任何东西。这叫作未定义行为(undefined behavior):程序可能崩溃,可能打印出乱码,也可能看起来一切正常,直到某天突然出问题。
所以调用它的规则是“只传入你确定有效的索引”。常见的做法是在一个普通的安全函数里,先亲自检查这条规则:
fn first_or_zero(numbers: &[i32]) -> i32 { if numbers.is_empty() { return 0; } // SAFETY: slice 不是空的,所以索引 0 一定在范围内。 unsafe { *numbers.get_unchecked(0) }}
fn main() { println!("{}", first_or_zero(&[7, 8, 9])); println!("{}", first_or_zero(&[]));}输出:
70看看调用者看到的是什么:fn first_or_zero(numbers: &[i32]) -> i32。签名里没有 unsafe,也没有办法用错它,因为函数在走到 unsafe 块之前就已经检查过规则了。unsafe 被缩到很小,藏在了函数内部。
有经验的 Rust 开发者都有两个习惯:
- 在每个
unsafe块上方写一行// SAFETY:注释,说明为什么规则得到了满足。这是写给下一个读代码的人看的,也是写给将来的你看的。 - 让
unsafe块尽可能小,这样需要反复核对的代码就更少。
说实话,真实项目里你不会写这个函数:numbers.first() 就能安全地完成同样的事,而且编译器很擅长去掉那些它能证明多余的边界检查。但这个模式(先检查规则,再在安全函数内部调用 unsafe)正是 Vec、String 这些类型的构建方式。FFI 那一页也用同样的技巧来包装 C 函数。
为什么要有 unsafe
Section titled “为什么要有 unsafe”既然 Rust 处处讲安全,为什么还允许这种东西?因为有些事情确实没法在编译期证明安全,尽管它们在实际中完全没问题:直接和硬件打交道、调用 C 库(见 FFI),或者实现某些高性能数据结构。有了 unsafe,标准库(偶尔也包括你自己的代码)才能实现这些底层部件,而建立在它们之上的一切,仍然是普通的、经过完整检查的安全 Rust。
实事求是地看取舍
Section titled “实事求是地看取舍”写了 unsafe 并不意味着你的代码就有 bug 了。它的意思是:在这个块里,编译器没法再帮你发现某几类错误,你得亲手维护同样的保证。大多数有经验的 Rust 开发者很少直接写 unsafe。它通常待在库的最底层,是几小块经过仔细审查的代码,外面包着一层安全的 API 供其他人使用。
1. 通过裸指针修改值
Section titled “1. 通过裸指针修改值”写一行 let mut score = 10;,创建一个指向它的 *mut i32 裸指针,然后通过这个指针给它加 5,最后打印 score。提示:用 &mut score as *mut i32 创建指针,在 unsafe 块里写 *pointer += 5。
查看答案
fn main() { let mut score = 10; let pointer = &mut score as *mut i32; // 创建指针是安全的
unsafe { *pointer += 5; // 通过它写入就不安全了 }
println!("score = {score}");}输出:
score = 15这段代码在实际中是安全的,因为 score 还活着,而且写入的时候没有别的代码在用它。一旦用了 unsafe,遵守这些承诺就是你的责任。
2. 安全的 get_or
Section titled “2. 安全的 get_or”写一个 fn get_or(numbers: &[i32], index: usize, default: i32) -> i32。它返回 index 位置的元素;如果索引越界,就返回 default。读取时用 get_unchecked,并确保这个函数不可能被以违反规则的方式调用。提示:先比较 index < numbers.len(),只有结果为真时才调用 get_unchecked。
查看答案
fn get_or(numbers: &[i32], index: usize, default: i32) -> i32 { if index < numbers.len() { // SAFETY: 刚刚检查过,index 在范围内。 unsafe { *numbers.get_unchecked(index) } } else { default }}
fn main() { let numbers = [10, 20, 30]; println!("{}", get_or(&numbers, 1, -1)); println!("{}", get_or(&numbers, 5, -1));}输出:
20-1索引 5 根本到不了 unsafe 块,所以会被安全地处理掉。作为对比,完全安全的写法是 numbers.get(index).copied().unwrap_or(default),平时你写的就应该是这个。
unsafe只解锁 5 种操作(主要是裸指针和调用 unsafe 函数),其他一切照样受到完整检查。- 裸指针(
*const T/*mut T)可以在安全代码里创建,解引用它才需要unsafe。 - 把
unsafe包在一个先检查规则的小型安全函数里,并用// SAFETY:注释说明它为什么是可靠的。 unsafe的存在是为了能写出底层代码(访问硬件、和 C 互操作),而使用这些代码的地方依然是安全的。- 作为新手,你很少需要自己写它,先找安全的抽象。
小测验
1. unsafe 块到底解锁了什么?
2. 能在安全代码里创建裸指针吗?
3. 作为新手,你需要自己写 unsafe 的频率大概是?
4. 写了 unsafe 代码,是不是就说明程序有 bug 了?
得分: 0 / 4
- 不安全 Rust — Rust 程序设计语言:详细介绍 5 种“unsafe 超能力”,包括如何在裸指针之上构建一个安全的
split_at_mut。 - The Rustonomicon(英文):教你正确编写 unsafe 代码的官方指南。内容比较深,但第一章非常值得一读。
unsafe关键字 — 标准库文档(英文):unsafe 块、unsafe 函数和 unsafe trait,以及// SAFETY:注释的惯例。- Unsafety — The Rust Reference(英文):需要
unsafe的操作的完整列表。 slice::get_unchecked— 标准库文档(英文):一个真实的 unsafe 函数,它的“Safety”一节写明了你必须遵守的规则。