unsafe:拆掉的是护栏,不是安全
关于 unsafe,流传最广的误解是:「它关掉了 Rust 的安全检查,让你切回 C。」这个理解错得相当彻底,也因此吓退了很多人。真相是:unsafe 只解锁五种特定的、编译器无法替你验证的操作,其余所有检查(包括借用检查)照常运行。它不让代码变安全 —— 它让你接过验证的责任,对编译器说一句「这块我人肉查过了,你放行」。
它到底关掉了什么(几乎什么都没关)
先破除那个最大的误解。在一个 unsafe 块里,下面这些全都照常工作,一个没关:
- 借用检查器 —— 照常。你在 unsafe 里写出「两个
&mut」,一样报错。 - 类型检查 —— 照常。类型不匹配一样编译不过。
- 所有权、移动、drop —— 照常。move 之后再用,一样
E0382。 - 生命周期 —— 照常。
unsafe 只做一件事:解锁下面这五种平时被禁止的操作。除此之外,Rust 的一切规矩纹丝不动。
一、解引用裸指针(*const T / *mut T,不受借用检查约束的原始指针)。
二、调用 unsafe 函数(包括通过 FFI 调用 C 函数)。
三、访问或修改可变静态变量(static mut)。
四、实现 unsafe trait(比如手动实现 Send / Sync)。
五、访问 union 的字段。
就这五个。你日常写的绝大多数 bug(借用、空指针、越界),unsafe 都不解锁 —— 它只开了这五扇编译器没法自动证明安全的小门。
那它为什么必须存在
因为借用检查器虽然强,但它是保守的:有些操作确实安全,但编译器证明不了。最典型的例子 —— 把一个数组切成两半、分别拿到两个可变引用:
let mut arr = [1, 2, 3, 4]; let (left, right) = arr.split_at_mut(2); // 一次拿到两个 &mut,分别指前后两半
你和我都知道这绝对安全 —— 前半段和后半段不重叠,两个 &mut 各管各的,没有别名。但借用检查器只看到「你想从一个数组同时搞出两个 &mut」,它没那么聪明去证明这两段不重叠,于是会拒绝。split_at_mut 的实现内部就用了 unsafe:它用裸指针手动切出两段,由标准库的作者向编译器担保「我数学上验证过这两段不重叠」。
这就是 unsafe 的本质:当「安全」这件事编译器算不出来、但人能证明时,让人来签这个字。它不是「我不管安全了」,恰恰相反 —— 它是「安全的责任,此刻从编译器转移到了我身上」。
Rust 的核心手法:把 unsafe 封进安全的壳
你可能会惊讶:标准库里到处是 unsafe。Vec、String、Rc、RefCell、HashMap…… 它们的内部实现都离不开裸指针和 unsafe,因为「管理一块手动分配的内存」本质上就是编译器无法全程验证的事。
但你用 Vec 的时候,从来不写 unsafe。为什么?因为这是 Rust 最重要的工程手法:
把 unsafe 的核心,包裹在一个经过严格验证、对外暴露安全接口的 API 里。库作者在那一小块 unsafe 里,人肉证明「只要外部按我的 API 用,就绝不会出问题」;于是所有使用者,都能在完全安全的世界里,享受底层 unsafe 的能力,一行 unsafe 都不用写。
Vec 就是典范:内部用裸指针管理堆缓冲区(unsafe),外面给你 push / get / 索引这些安全方法,还替你保证越界会 panic 而不是读野内存。你站在护栏内,护栏本身用了一点护栏外的材料来打造。
破了约定会怎样:未定义行为
unsafe 的严肃之处在于:一旦你在 unsafe 里违反了那些编译器没帮你查的不变式(比如解引用了一个悬垂的裸指针、造出了两个真正重叠的 &mut),你就触发了未定义行为(UB)—— 程序可以做任何事:崩溃、读到垃圾、看起来正常然后在别处随机出错、甚至被攻击者利用。这正是 C/C++ 整天面对的世界。
重申第 2 章的告诫,因为它在这里最危险:新手有时会想「借用检查器不让我这么写,那我用 unsafe 绕过去」。这是最糟的用法。第一,unsafe 根本不解锁借用检查(它照常运行),你多半绕不过去;第二,就算你用裸指针硬绕,你只是亲手制造了一个 use-after-free 的定时炸弹 —— 把编译器好心拦下的 bug,又放了回去。
unsafe 的正当用途极窄:写底层数据结构、和 C 库互操作(FFI)、做编译器证明不了但你能证明的极致优化。对 99% 的应用开发者,你可能整个职业生涯都不需要写一行 unsafe。需要它的时候,它是精密工具;不需要它的时候,别碰。
回流
C / C++:整门语言时时刻刻都是 unsafe —— 每一次指针解引用、每一次数组访问、每一次内存管理,都可能是 UB,而且编译器基本不拦。所以 C/C++ 项目的安全审计是场噩梦:危险无处不在,无从聚焦。Rust 反过来:默认安全,危险被关进一个个显式标注的 unsafe 块里。这意味着审计时你可以 grep "unsafe",把注意力精确聚焦到那极少数几处 —— 剩下 95% 的代码,编译器已经证明了内存安全。这才是 Rust 安全性的真正含义:不是「没有危险操作」,而是「危险操作被隔离、被标注、被最小化、可被审计」。业界大量把 C 代码逐步换成 Rust,图的正是这个 —— 把「满地雷」变成「雷都插了旗」。
Java / Kotlin:JVM 语言没有 unsafe 这个层级的东西(sun.misc.Unsafe 是另一回事,且被逐步封死),因为它们靠 GC 和运行时检查兜底,代价是性能和「贴近硬件」的能力。Rust 的 unsafe 是它能又安全又贴近底层的关键机关:绝大多数时候享受安全,极少数需要下探到硬件的地方,用一扇标注清楚的门下去。
这一章的一句话
unsafe 不关闭借用检查,它只解锁五种编译器无法验证的操作,把安全的责任从编译器转移到你。Rust 的手法是把 unsafe 的核心封进安全的 API —— 于是危险被隔离、标注、最小化,可以被 grep、被审计。你很可能永远不需要写它。
卷 IV 走完了 —— 你现在诚实地知道了「一个所有者不够用」时的每一条退路,以及它们各自的代价。接下来是很多人对 Rust 最期待、也最好奇的一卷:并发。而它最神奇的地方在于 —— 它没有发明任何新规则。数据竞争,就是你已经熟得不能再熟的借用铁律,被原样搬到了多线程。