📚 Rust 课程系列

  1. 课程概览
  2. 基础语法
  3. 函数与输入输出
  4. 所有权、借用与生命周期
  5. 结构体、枚举与模式匹配
  6. 类型系统:泛型、trait 与多态
  7. 集合与容器
  8. 错误处理与 Panic 恢复
  9. 模块、属性与宏
  10. 智能指针、迭代器与闭包
  11. 并发与异步编程
  12. Unsafe Rust 与常用 trait 详解(本文)
  13. 工具链、Cargo 与外部 crate
  14. 最佳实践、性能与调试

本章覆盖两个进阶主题: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 traitSend/Sync 的手动实现
访问 union 字段读取 union 的字段是不安全的
1
2
3
4
5
6
7
8
let mut num = 5;
let r1 = &num as *const i32; // 创建裸指针本身是安全的
let r2 = &mut num as *mut i32;

unsafe {
println!("r1 is: {}", *r1); // 解引用必须在 unsafe 块中
println!("r2 is: {}", *r2);
}

⚠️ 注意:创建裸指针(&x as *const T)是安全操作,只有解引用才需要 unsafe。这体现了 Rust 的设计哲学——声明意图不危险,实际访问才需要担保。

unsafe 函数

1
2
3
4
5
unsafe fn dangerous() {
// 调用者必须保证安全性前提条件
}

unsafe { dangerous(); } // 调用必须在 unsafe 块中

🔄 对比:C/C++ 中所有函数默认都是"unsafe"的——编译器不区分安全与不安全调用。Rust 通过 unsafe fn 将不安全接口显式标记,调用者必须用 unsafe 块确认"我知道我在做什么"。

外部函数接口(FFI)

1
2
3
4
5
extern "C" {
fn abs(input: i32) -> i32; // 声明外部 C 函数
}

let x = unsafe { abs(-3) }; // 调用必须在 unsafe 块中

CStr 与 CString:在 FFI 中安全地传递 C 字符串。

1
2
3
4
5
6
7
8
use std::ffi::CString;

extern "C" {
fn puts(s: *const libc::c_char);
}

let cstring = CString::new("Hello from Rust!").unwrap();
unsafe { puts(cstring.as_ptr()); }

💡 提示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
2
3
4
let mut value = 42;
let ptr = &mut value as *mut i32;
let mutable_ref = unsafe { &mut *ptr }; // 从裸指针创建可变引用
*mutable_ref = 100;

⚠️ 注意:从裸指针创建引用(&mut *ptr)时,你必须保证没有其他引用同时指向该内存,否则违反借用规则导致未定义行为——编译器不会帮你检查。

联合体(Union)

所有字段共享同一内存空间,访问字段必须在 unsafe 块中。

1
2
3
4
5
6
7
8
9
10
11
#[repr(C)]
union IntOrFloat {
as_int: i32,
as_float: f32,
}

let u = IntOrFloat { as_int: 42 };
unsafe {
println!("as int: {}", u.as_int); // 安全:与写入的是同一字段
println!("as float: {}", u.as_float); // 未定义行为:类型不匹配
}

🔄 对比: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(进阶)

SendSync 是标准库中最重要的两个 marker trait,编译器会根据类型的组成自动推导。但当你需要手动声明一个类型可以跨线程共享时,必须用 unsafe impl

1
2
3
4
5
struct MyRc<T> { ptr: *mut T }  // 内部用裸指针,编译器不会自动推导 Send/Sync

// 安全性保证:MyRc 内部有原子引用计数,确实可以跨线程共享
unsafe impl<T: Send + Sync> Send for MyRc<T> {}
unsafe impl<T: Send + Sync> Sync for MyRc<T> {}

🔬 进阶Send 表示"类型可以安全地移动到另一个线程"(所有权转移),Sync 表示"类型可以安全地被多个线程共享引用"(&TSend 的)。二者关系: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
2
3
4
5
6
7
8
9
10
11
use std::ops::Deref;

struct MyBox<T>(T);

impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target { &self.0 }
}

let y = MyBox(5);
assert_eq!(5, *y); // *y 实际调用 *(y.deref())

Deref 强制转换(deref coercion):当类型实现了 Deref,编译器会自动将 &MyBox<T> 转换为 &T。这让智能指针可以像普通引用一样使用。

💡 提示Deref 强制转换只发生在函数参数和方法调用中,不会在赋值时触发。常用于 Box<T>Rc<T>Arc<T> 等智能指针。

⚠️ 注意:不要为了方法查找便利而滥用 Deref——它不是继承的替代品。Deref 应只用于"拥有并代理内部类型"的智能指针模式,否则会让代码难以理解。

Drop — 自定义析构

1
2
3
4
5
6
7
8
9
10
struct File { name: String }

impl Drop for File {
fn drop(&mut self) {
println!("Dropping file: {}", self.name);
}
}

let _f = File { name: "data.txt".to_string() };
// 离开作用域时自动调用 drop

💡 提示

  • 字段按声明顺序逆序 drop(与 C++ 析构顺序一致)
  • 不能手动调用 drop 方法,应使用 std::mem::drop 函数(它只是移动值然后让其自然 drop)
  • DropCopy 互斥——Copy 类型按位复制,不需要析构逻辑

🔄 对比:Rust 的 Drop ≈ C++ 的析构函数,但 Rust 保证确定性析构(离开作用域立即执行),无 GC 延迟。与 Go 的 defer 不同,Drop 是隐式绑定的,不需要手动注册。

Default — 默认值

1
2
3
4
5
6
7
8
#[derive(Default)]
struct Config {
host: String, // 默认 ""
port: u16, // 默认 0
}

let config = Config::default();
let custom = Config { port: 8080, ..Default::default() }; // 结构体更新语法

💡 提示..Default::default() 是 Rust 中"只覆盖部分字段"的惯用写法,类似 JS 的 {...defaults, port: 8080}Option<T>default()None,这在配置解析中非常实用。

From / Into — 类型转换

1
2
3
4
5
6
7
8
9
10
struct Point { x: i32, y: i32 }

impl From<(i32, i32)> for Point {
fn from(coords: (i32, i32)) -> Self {
Point { x: coords.0, y: coords.1 }
}
}

let p1 = Point::from((10, 20)); // 显式调用
let p2: Point = (30, 40).into(); // Into 自动获得

💡 提示:实现 From 会自动获得 Into(反之不行),所以只实现 From? 运算符依赖 From 做错误类型转换——为自定义错误实现 From<底层错误> 后,? 就能自动传播。

⚠️ 注意From/Into 用于无损转换。如果有信息丢失(如 f64 → f32),应使用 TryFrom/TryInto

AsRef / AsMut — 借用转换

1
2
3
4
5
6
7
fn process_path<P: AsRef<std::path::Path>>(path: P) {
let path = path.as_ref();
// 使用 path...
}

process_path("/home/user"); // &str → &Path
process_path(std::path::Path::new("data.txt")); // &Path → &Path

💡 提示AsRef vs Borrow vs DerefAsRef 是轻量级借用转换(&self → &U),Borrow 用于 HashMap/BTreeMap 键的等价性(要求 Hash/Eq 一致),Deref 用于智能指针的自动强制转换。函数参数接受多种类型时优先用 AsRef

FromStr / ToString — 字符串转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::str::FromStr;

struct Point { x: i32, y: i32 }

impl FromStr for Point {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let coords: Vec<i32> = s.split(',')
.map(|n| n.parse().map_err(|_| "Invalid number".to_string()))
.collect::<Result<_, _>>()?;
match coords.as_slice() {
[x, y] => Ok(Point { x: *x, y: *y }),
_ => Err("Expected exactly two numbers".to_string()),
}
}
}

let p = "10,20".parse::<Point>().unwrap(); // FromStr 让 parse() 工作
let s = p.to_string(); // Display → ToString

💡 提示:实现 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 详解》