
编译器:我不允许!你:我偏要!欢迎来到 Rust 的"法外之地"
学到这里,你可能已经对 Rust 的借用检查器又爱又恨了。爱它帮你避免了无数内存 bug,恨它有时候管得实在太宽——"这个引用不能同时存在"、"那个变量已经移动了"、"这个生命周期我不认识"……
有时候,你明明知道自己在做什么,但编译器就是不让。这时候,你可能想对着屏幕大喊:"我才是程序员!我说了算!"
恭喜你,这种想法说明你已经准备好了学习 unsafe Rust。
但先别急着兴奋。unsafe Rust 不是让你"为所欲为"的通行证,而是一把双刃剑。用好了,你能写出高性能的底层代码;用不好……嗯,segmentation fault 在向你招手。
今天,咱们就来聊聊 Rust 中这个既危险又迷人的特性。
首先纠正一个误区:unsafe 不代表代码一定不安全,而是说编译器无法保证安全。
想象一下:
安全 Rust 就像一个严格的保镖,你做的每一步它都要检查:"这个安全吗?那个合法吗?" unsafe Rust 就像你签了一份免责声明:"我知道有风险,出了问题我负责,你别管了。"
Rust 有四个操作只能在 unsafe 块中使用:
既然 unsafe 这么危险,为什么 Rust 还要提供它?
原因很简单:有些操作无法在安全的前提下实现。
比如:
事实上,Rust 标准库内部也用了不少 unsafe 代码。关键是:把 unsafe 封装在安全的接口里。
裸指针是 unsafe Rust 的入门课。它和普通引用很像,但有几个关键区别:
fn main() {
// 创建普通引用
let x = ;
let ref_x = &x;
// 创建裸指针(需要 unsafe 吗?不需要!创建是安全的)
let ptr_x = &x as *const i32; // 不可变裸指针
let mut y = ;
let ptr_y = &mut y as *mut i32; // 可变裸指针
println!("ptr_x: {:?}", ptr_x);
println!("ptr_y: {:?}", ptr_y);
// 解引用裸指针需要 unsafe!
unsafe {
println!("*ptr_x = {}", *ptr_x); // 读取
*ptr_y = ; // 修改
println!("*ptr_y = {}", *ptr_y);
}
}
裸指针 vs 普通引用:
特性 | 普通引用 &T | 裸指针 *const T / *mut T |
|---|---|---|
安全性检查 | 编译器保证 | 程序员负责 |
可以为 null | ❌ 不可以 | ✅ 可以 |
可以算术运算 | ❌ 不可以 | ✅ 可以 |
需要 lifetime | ✅ 需要 | ❌ 不需要 |
解引用 | 安全 | 需要 unsafe |
fn main() {
// 危险操作 1:创建空指针
let null_ptr: *const i32 = std::ptr::null();
// 危险操作 2:悬垂指针(指针指向的内存已经释放)
let dangling_ptr: *const i32;
{
let x = ;
dangling_ptr = &x as *const i32;
// x 在这里被 drop 了
}
// 危险操作 3:访问无效内存
unsafe {
// println!("{}", *dangling_ptr); // 未定义行为!别取消注释
// println!("{}", *null_ptr); // 程序崩溃!
}
// 正确的裸指针使用:访问数组元素
let arr = [, , , , ];
let ptr = arr.as_ptr();
unsafe {
for i in ..arr.len() {
// 指针算术运算
let elem_ptr = ptr.add(i);
println!("arr[{}] = {}", i, *elem_ptr);
}
}
}
编译器吐槽时间:
error[E0133]: call to unsafe function is unsafe and requires unsafe block
--> src/main.rs:15:9
|
15 | println!("{}", *dangling_ptr);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: consult the function's documentation for information on how to avoid undefined behavior
编译器:"我知道你想干什么,但出了事别找我。"
在安全 Rust 中,全局可变状态是禁止的。但 unsafe 允许你创建 static mut:
// 全局可变计数器
static mut COUNTER: i32 = ;
fn increment_counter() {
unsafe {
COUNTER += ;
}
}
fn get_counter() -> i32 {
unsafe {
COUNTER
}
}
fn main() {
increment_counter();
increment_counter();
increment_counter();
unsafe {
println!("Counter: {}", COUNTER); // 输出:3
}
}
⚠️ 警告: 这在多线程环境下会导致数据竞争!
use std::thread;
static mut SHARED_DATA: i32 = ;
fn main() {
let mut handles = vec![];
for _ in .. {
let handle = thread::spawn(|| {
unsafe {
// 数据竞争!多个线程同时修改同一个变量
SHARED_DATA += ;
}
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
unsafe {
println!("Result: {}", SHARED_DATA); // 可能是 10,也可能是 7,谁知道呢?
}
}
正确的做法: 使用 Mutex 或 Atomic 类型(后面并发篇会讲)。
你可以标记自己的函数为 unsafe,调用者必须用 unsafe 块包裹:
// 这个函数要求调用者保证索引有效
unsafe fn get_unchecked(slice: &[i32], index: usize) -> i32 {
*slice.as_ptr().add(index)
}
fn main() {
let data = [, , , , ];
// 安全调用(索引有效)
unsafe {
let value = get_unchecked(&data, );
println!("Value: {}", value); // 3
}
// 危险调用(索引越界)
unsafe {
// let value = get_unchecked(&data, 100); // 未定义行为!
}
}
最佳实践: 为 unsafe 函数提供清晰的安全条件文档:
/// # Safety
///
/// 调用者必须保证:
/// 1. index < slice.len()
/// 2. slice 在函数调用期间保持不变
/// 3. 指针解引用不会导致未定义行为
unsafe fn get_unchecked(slice: &[i32], index: usize) -> i32 {
*slice.as_ptr().add(index)
}
某些 trait 的实现可能破坏 Rust 的安全保证,需要标记为 unsafe:
// 标记一个 trait 为 unsafe
unsafe trait CanBeDropped {}
// 实现 unsafe trait 也需要 unsafe
struct MyResource {
data: Vec<u8>,
}
unsafe impl CanBeDropped for MyResource {}
fn drop_resource<T: CanBeDropped>(resource: T) {
drop(resource);
}
fn main() {
let resource = MyResource { data: vec![, , ] };
drop_resource(resource);
}
为什么需要 unsafe trait?
最典型的例子是 Send 和 Sync trait:
// Send: 可以安全地移动到另一个线程
// Sync: 可以安全地在多个线程中共享引用
// 这个类型可以安全地发送到其他线程
struct Data {
value: i32,
}
unsafe impl Send for Data {} // 我们保证它是线程安全的
// 但这个不行!
struct NotThreadSafe {
raw_ptr: *mut i32, // 裸指针,多线程访问会出问题
}
// 如果你错误地标记为 Send,编译器也不会阻止你
// unsafe impl Send for NotThreadSafe {} // 危险!
fn main() {
let x = ;
let ptr = &x as *const i32;
unsafe {
// 你以为这样可以修改不可变变量?
// *(ptr as *mut i32) = 10; // 未定义行为!
}
}
真相: unsafe 不能改变物理定律。修改通过不可变引用创建的裸指针,仍然是未定义行为。
unsafe fn dangerous() {
println!("I'm dangerous!");
}
fn main() {
// dangerous(); // 错误!需要 unsafe 块
unsafe {
dangerous();
}
}
// ❌ 坏例子:明明可以用安全代码
fn bad_example(x: &mut i32) {
unsafe {
let ptr = x as *mut i32;
*ptr += ;
}
}
// ✅ 好例子:直接用安全代码
fn good_example(x: &mut i32) {
*x += ;
}
原则: unsafe 应该是最小化的,并且封装在安全接口内部。
让我们看看 unsafe 在实际中的应用。这是简化版的 Vec 实现:
use std::alloc::{alloc, dealloc, Layout};
use std::ptr;
struct SimpleVec<T> {
ptr: *mut T,
length: usize,
capacity: usize,
}
impl<T> SimpleVec<T> {
fn new() -> Self {
SimpleVec {
ptr: ptr::null_mut(),
length: ,
capacity: ,
}
}
fn push(&mut self, value: T) {
if self.length == self.capacity {
// 需要扩容
if self.capacity == {
self.capacity = ;
} else {
self.capacity *= ;
}
unsafe {
// 分配新内存
let new_layout = Layout::array::<T>(self.capacity).unwrap();
let new_ptr = alloc(new_layout) as *mut T;
// 复制旧数据
if self.length > {
ptr::copy_nonoverlapping(
self.ptr,
new_ptr,
self.length,
);
}
// 释放旧内存
if self.capacity > {
let old_layout = Layout::array::<T>(self.capacity / ).unwrap();
dealloc(self.ptr as *mut u8, old_layout);
}
self.ptr = new_ptr;
}
}
unsafe {
// 写入新元素
ptr::write(self.ptr.add(self.length), value);
}
self.length += ;
}
fn get(&self, index: usize) -> Option<&T> {
if index < self.length {
unsafe {
Some(&*self.ptr.add(index))
}
} else {
None
}
}
}
impl<T> Drop for SimpleVec<T> {
fn drop(&mut self) {
if self.capacity > {
unsafe {
// 释放所有元素
for i in ..self.length {
ptr::drop_in_place(self.ptr.add(i));
}
// 释放内存
let layout = Layout::array::<T>(self.capacity).unwrap();
dealloc(self.ptr as *mut u8, layout);
}
}
}
}
fn main() {
let mut vec = SimpleVec::new();
vec.push();
vec.push();
vec.push();
println!("vec[0] = {:?}", vec.get()); // Some(1)
println!("vec[1] = {:?}", vec.get()); // Some(2)
println!("vec[2] = {:?}", vec.get()); // Some(3)
}
这个例子展示了:

金句时间:
unsafe Rust 就像核能——用好了能发电,用不好能灭世。
学完 unsafe,你是不是想:"我终于可以和 C 代码手牵手了!"
没错,下篇咱们就来聊聊 FFI(外部函数接口),看看 Rust 怎么和 C 语言互操作。届时你会看到:
敬请期待 第 36 篇:FFI!