Unsafe Rust 与常用 trait 详解
📚 Rust 课程系列
本章覆盖两个进阶主题:unsafe Rust——当你必须绕过编译器安全检查进行底层操作时的逃生舱;以及常用标准 trait 的实现要点——理解它们是写出惯用 Rust 代码的关键。unsafe 不是「关闭安全」,而是把安全的责任从编译器转移到开发者。
不安全 Rust(Unsafe Rust)
当 Rust 的安全检查无法表达某些底层操作时,unsafe 关键字允许你绕过编译器的检查。使用 unsafe 时,你必须保证代码的安全性,编译器信任你的判断。
💡 提示:
unsafe并不会关闭借用检查或关闭 panic,它只是解锁以下五类操作。即使在一个unsafe块里,常规的安全 Rust 规则依然适用。unsafe的边界越小越好,便于审查。
unsafe 块与五类操作
| 操作 | 说明 |
|---|---|
| 解引用裸指针 | *ptr 读取/写入裸指针指向的值 |
| 调用 unsafe 函数 | 必须在 unsafe 块中调用 unsafe fn |
| 访问/修改可变静态变量 | static mut 的读写 |
| 实现 unsafe trait | 如 Send/Sync 的手动实现 |
| 访问 union 字段 | 读取 union 的字段是不安全的 |
1 | |
⚠️ 注意:创建裸指针(
&x as *const T)是安全操作,只有解引用才需要unsafe。这体现了 Rust 的设计哲学——声明意图不危险,实际访问才需要担保。
unsafe 函数
1 | |
🔄 对比:C/C++ 中所有函数默认都是"unsafe"的——编译器不区分安全与不安全调用。Rust 通过
unsafe fn将不安全接口显式标记,调用者必须用unsafe块确认"我知道我在做什么"。
外部函数接口(FFI)
1 | |
CStr 与 CString:在 FFI 中安全地传递 C 字符串。
1 | |
💡 提示:
CString保证内部字节以\0结尾且不含嵌入的\0(否则new返回错误)。CStr是对已有 C 字符串的借用视图,不拥有内存。从 Rust 传字符串给 C 用CString,从 C 接收字符串用CStr。
裸指针类型
*const T(不可变)和 *mut T(可变),与引用的关键区别:
| 特性 | 引用 &T/&mut T | 裸指针 *const T/*mut T |
|---|---|---|
| 借用检查 | 编译器强制执行 | 无 |
| 空值 | 不允许 | 允许(ptr::null()) |
| Send/Sync | 自动推导 | 不检查,可跨线程共享 |
| 解引用 | 安全 | 必须在 unsafe 块中 |
1 | |
⚠️ 注意:从裸指针创建引用(
&mut *ptr)时,你必须保证没有其他引用同时指向该内存,否则违反借用规则导致未定义行为——编译器不会帮你检查。
联合体(Union)
所有字段共享同一内存空间,访问字段必须在 unsafe 块中。
1 | |
🔄 对比:Rust 的
union与 C 的union语义一致,但强制unsafe访问。Rust 更推荐用enum替代 union——enum提供类型安全的标签联合体。
unsafe 代码的最佳实践
⚠️ 注意:
- 尽量将
unsafe代码隔离在小型、独立的函数或模块中,安全抽象包裹不安全实现- 每个
unsafe块务必写注释说明为何安全(不是"做了什么",而是"为什么不会违反不变量")- 典型模式:
Vec<T>内部大量使用unsafe,但对外暴露完全安全的 API- 使用
ptr::write/ptr::read时注意 strict provenance 规则——裸指针不仅是一个地址,还携带来源信息
Send、Sync 与 unsafe impl(进阶)
Send 和 Sync 是标准库中最重要的两个 marker trait,编译器会根据类型的组成自动推导。但当你需要手动声明一个类型可以跨线程共享时,必须用 unsafe impl:
1 | |
🔬 进阶:
Send表示"类型可以安全地移动到另一个线程"(所有权转移),Sync表示"类型可以安全地被多个线程共享引用"(&T是Send的)。二者关系:T: Sync⟺&T: Send。裸指针*const T/*mut T默认既不Send也不Sync,因为编译器无法证明跨线程安全。
常用 trait 详解
Rust 标准库定义了许多重要的 trait,理解它们对于编写惯用代码至关重要。标记 trait(Sized/Send/Sync/Unpin)和派生 trait(Clone/Copy/Debug 等)的完整讨论见《Rust 标准 Trait 详解》。
Deref — 自定义解引用
1 | |
Deref 强制转换(deref coercion):当类型实现了 Deref,编译器会自动将 &MyBox<T> 转换为 &T。这让智能指针可以像普通引用一样使用。
💡 提示:
Deref强制转换只发生在函数参数和方法调用中,不会在赋值时触发。常用于Box<T>、Rc<T>、Arc<T>等智能指针。
⚠️ 注意:不要为了方法查找便利而滥用
Deref——它不是继承的替代品。Deref应只用于"拥有并代理内部类型"的智能指针模式,否则会让代码难以理解。
Drop — 自定义析构
1 | |
💡 提示:
- 字段按声明顺序逆序 drop(与 C++ 析构顺序一致)
- 不能手动调用
drop方法,应使用std::mem::drop函数(它只是移动值然后让其自然 drop)Drop和Copy互斥——Copy类型按位复制,不需要析构逻辑
🔄 对比:Rust 的
Drop≈ C++ 的析构函数,但 Rust 保证确定性析构(离开作用域立即执行),无 GC 延迟。与 Go 的defer不同,Drop是隐式绑定的,不需要手动注册。
Default — 默认值
1 | |
💡 提示:
..Default::default()是 Rust 中"只覆盖部分字段"的惯用写法,类似 JS 的{...defaults, port: 8080}。Option<T>的default()是None,这在配置解析中非常实用。
From / Into — 类型转换
1 | |
💡 提示:实现
From会自动获得Into(反之不行),所以只实现From。?运算符依赖From做错误类型转换——为自定义错误实现From<底层错误>后,?就能自动传播。
⚠️ 注意:
From/Into用于无损转换。如果有信息丢失(如f64 → f32),应使用TryFrom/TryInto。
AsRef / AsMut — 借用转换
1 | |
💡 提示:
AsRefvsBorrowvsDeref:AsRef是轻量级借用转换(&self → &U),Borrow用于HashMap/BTreeMap键的等价性(要求Hash/Eq一致),Deref用于智能指针的自动强制转换。函数参数接受多种类型时优先用AsRef。
FromStr / ToString — 字符串转换
1 | |
💡 提示:实现
Display会自动获得ToString(不要手动实现ToString)。FromStr让"...".parse::()` 工作。二者互为逆操作。
标准库还有大量重要 trait 未在此展开,如标记 trait Sized/Send/Sync/Unpin、派生 trait Clone/Copy/Debug/Display/PartialEq/Eq/PartialOrd/Ord/Hash、迭代 trait Iterator/IntoIterator/FromIterator、I/O trait Read/Write/Seek/BufRead、错误 trait Error 等。它们的完整定义、实现要点与代码示例见延伸阅读《Rust 标准 Trait 详解》。


