首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >35-Rust 教程 - unsafe Rust

35-Rust 教程 - unsafe Rust

作者头像
LarryLan
发布2026-06-04 12:04:17
发布2026-06-04 12:04:17
560
举报

unsafe Rust

编译器:我不允许!你:我偏要!欢迎来到 Rust 的"法外之地"

🎬 引入

学到这里,你可能已经对 Rust 的借用检查器又爱又恨了。爱它帮你避免了无数内存 bug,恨它有时候管得实在太宽——"这个引用不能同时存在"、"那个变量已经移动了"、"这个生命周期我不认识"……

有时候,你明明知道自己在做什么,但编译器就是不让。这时候,你可能想对着屏幕大喊:"我才是程序员!我说了算!"

恭喜你,这种想法说明你已经准备好了学习 unsafe Rust

但先别急着兴奋。unsafe Rust 不是让你"为所欲为"的通行证,而是一把双刃剑。用好了,你能写出高性能的底层代码;用不好……嗯,segmentation fault 在向你招手。

今天,咱们就来聊聊 Rust 中这个既危险又迷人的特性。

📌 核心概念:什么是 unsafe?

unsafe 不是"不安全",而是"我负责"

首先纠正一个误区:unsafe 不代表代码一定不安全,而是说编译器无法保证安全

想象一下:

安全 Rust 就像一个严格的保镖,你做的每一步它都要检查:"这个安全吗?那个合法吗?" unsafe Rust 就像你签了一份免责声明:"我知道有风险,出了问题我负责,你别管了。"

Rust 有四个操作只能在 unsafe 块中使用:

  1. 解引用裸指针(Dereference a raw pointer)
  2. 调用 unsafe 函数或方法(Call an unsafe function or method)
  3. 访问或修改可变静态变量(Access or modify a mutable static variable)
  4. 实现 unsafe trait(Implement an unsafe trait)

为什么需要 unsafe?

既然 unsafe 这么危险,为什么 Rust 还要提供它?

原因很简单:有些操作无法在安全的前提下实现

比如:

  • 与 C 代码互操作(FFI)
  • 访问硬件或操作系统底层 API
  • 实现某些高性能数据结构(如 Vec 的内部实现)
  • 突破借用检查器的限制(当你确定安全时)

事实上,Rust 标准库内部也用了不少 unsafe 代码。关键是:把 unsafe 封装在安全的接口里

💻 代码示例

1. 裸指针(Raw Pointers)

裸指针是 unsafe Rust 的入门课。它和普通引用很像,但有几个关键区别:

代码语言:javascript
复制
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

2. 裸指针的危险操作

代码语言:javascript
复制
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);
        }
    }
}

编译器吐槽时间:

代码语言:javascript
复制
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

编译器:"我知道你想干什么,但出了事别找我。"

3. 可变静态变量

在安全 Rust 中,全局可变状态是禁止的。但 unsafe 允许你创建 static mut

代码语言:javascript
复制
// 全局可变计数器
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
    }
}

⚠️ 警告: 这在多线程环境下会导致数据竞争!

代码语言:javascript
复制
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,谁知道呢?
    }
}

正确的做法: 使用 MutexAtomic 类型(后面并发篇会讲)。

4. unsafe 函数

你可以标记自己的函数为 unsafe,调用者必须用 unsafe 块包裹:

代码语言:javascript
复制
// 这个函数要求调用者保证索引有效
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 函数提供清晰的安全条件文档:

代码语言:javascript
复制
/// # Safety
/// 
/// 调用者必须保证:
/// 1. index < slice.len()
/// 2. slice 在函数调用期间保持不变
/// 3. 指针解引用不会导致未定义行为
unsafe fn get_unchecked(slice: &[i32], index: usize) -> i32 {
    *slice.as_ptr().add(index)
}

5. unsafe Trait

某些 trait 的实现可能破坏 Rust 的安全保证,需要标记为 unsafe

代码语言:javascript
复制
// 标记一个 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?

最典型的例子是 SendSync trait:

代码语言:javascript
复制
// Send: 可以安全地移动到另一个线程
// Sync: 可以安全地在多个线程中共享引用

// 这个类型可以安全地发送到其他线程
struct Data {
    value: i32,
}

unsafe impl Send for Data {}  // 我们保证它是线程安全的

// 但这个不行!
struct NotThreadSafe {
    raw_ptr: *mut i32,  // 裸指针,多线程访问会出问题
}

// 如果你错误地标记为 Send,编译器也不会阻止你
// unsafe impl Send for NotThreadSafe {}  // 危险!

🐛 常见坑点

坑点 1:以为 unsafe 可以绕过所有检查

代码语言:javascript
复制
fn main() {
    let x = ;
    let ptr = &x as *const i32;
    
    unsafe {
        // 你以为这样可以修改不可变变量?
        // *(ptr as *mut i32) = 10;  // 未定义行为!
    }
}

真相: unsafe 不能改变物理定律。修改通过不可变引用创建的裸指针,仍然是未定义行为。

坑点 2:忘记 unsafe 块

代码语言:javascript
复制
unsafe fn dangerous() {
    println!("I'm dangerous!");
}

fn main() {
    // dangerous();  // 错误!需要 unsafe 块
    unsafe {
        dangerous();
    }
}

坑点 3:过度使用 unsafe

代码语言:javascript
复制
// ❌ 坏例子:明明可以用安全代码
fn bad_example(x: &mut i32) {
    unsafe {
        let ptr = x as *mut i32;
        *ptr += ;
    }
}

// ✅ 好例子:直接用安全代码
fn good_example(x: &mut i32) {
    *x += ;
}

原则: unsafe 应该是最小化的,并且封装在安全接口内部。

🎯 实战案例:实现一个简单的 Vec

让我们看看 unsafe 在实际中的应用。这是简化版的 Vec 实现:

代码语言:javascript
复制
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 块的正确使用
  • 将 unsafe 封装在安全接口中

🧠 思维导图

35-unsafe Rust
35-unsafe Rust

📝 小结

  1. unsafe 不是"不安全",而是"编译器无法验证,程序员负责"
  2. 四个 unsafe 操作: 解引用裸指针、调用 unsafe 函数、访问可变静态变量、实现 unsafe trait
  3. 裸指针很强大,但也很危险——可以为 null、可以算术运算、没有生命周期
  4. static mut 在多线程下会导致数据竞争——用 Mutex 或 Atomic 代替
  5. 最佳实践: 最小化 unsafe,封装在安全接口内,详细文档化安全条件

金句时间:

unsafe Rust 就像核能——用好了能发电,用不好能灭世。

🔗 下篇预告

学完 unsafe,你是不是想:"我终于可以和 C 代码手牵手了!"

没错,下篇咱们就来聊聊 FFI(外部函数接口),看看 Rust 怎么和 C 语言互操作。届时你会看到:

  • 怎么调用 C 函数
  • 怎么让 C 调用 Rust 函数
  • 怎么安全地封装 FFI 接口

敬请期待 第 36 篇:FFI

🔗 参考资料

  • The Rustonomicon - unsafe Rust
  • Rust Book - unsafe Rust
  • Rust by Example - unsafe
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-06-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Larry的Hub 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • unsafe Rust
    • 🎬 引入
    • 📌 核心概念:什么是 unsafe?
      • unsafe 不是"不安全",而是"我负责"
      • 为什么需要 unsafe?
    • 💻 代码示例
      • 1. 裸指针(Raw Pointers)
      • 2. 裸指针的危险操作
      • 3. 可变静态变量
      • 4. unsafe 函数
      • 5. unsafe Trait
    • 🐛 常见坑点
      • 坑点 1:以为 unsafe 可以绕过所有检查
      • 坑点 2:忘记 unsafe 块
      • 坑点 3:过度使用 unsafe
    • 🎯 实战案例:实现一个简单的 Vec
    • 🧠 思维导图
    • 📝 小结
    • 🔗 下篇预告
    • 🔗 参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档