一、所有权:Rust 最核心的创新

Rust 之所以能在系统编程领域脱颖而出,其所有权系统(Ownership System)功不可没。这一机制在编译期就消除了数据竞争、空指针解引用和悬垂指针等常见内存错误,无需运行时垃圾回收。理解所有权,是写出地道 Rust 代码的第一步。

所有权的铁律只有三条:

  1. Rust 中的每个值都有一个变量作为其所有者
  2. 同一时刻,每个值只能有一个所有者
  3. 当所有者离开作用域,其值会被自动释放(调用 drop)
fn ownership_demo() {
    let s1 = String::from("hello");
    let s2 = s1;            // s1 的所有权移动到 s2,s1 不再有效
    // println!("{}", s1);  // 编译错误!
    println!("{}", s2);     // 正确:s2 是唯一所有者
} // s2 离开作用域,内存被释放

二、借用与引用:共享 vs 独占

移动语义虽然安全,但不方便。Rust 的借用(Borrowing)机制允许你临时使用值而不获取所有权:

  • 不可变引用 &T:可以同时存在多个,但在此期间不能修改值
  • 可变引用 &mut T:同一时刻只能有一个,且对其他引用(不可变或可变)互斥
fn borrow_demo() {
    let mut data = vec![1, 2, 3];
    
    let ref1 = &data;       // 不可变借用
    let ref2 = &data;       // 可以同时存在多个不可变借用
    println!("{} {}", ref1[0], ref2[0]);
    
    // 不可变借用结束后才能获取可变借用
    let ref_mut = &mut data; 
    ref_mut.push(4);        // 独占访问,安全修改
}

借用检查器会在编译期验证这些规则,任何违反都会在编译时报错——这是 Rust 内存安全保障的第一道防线。

三、生命周期:让引用永不过期

当函数返回引用时,编译器需要确定该引用的有效期。生命周期标注(Lifetime Annotation)就是在告诉编译器引用之间的关系,确保不会产生悬垂引用。

// 显式生命周期标注
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

// 'a 表示:返回值的生命周期与两个输入参数中较短者一样长
fn demo() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
        // string2 在这里离开作用域,result 如果引用 string2 就会无效
    }
    println!("The longest is {}", result); // 如果 result 引用 string2 就会编译失败
}

结构体中的生命周期标注同理——当结构体持有引用时,必须标注结构体实例不能比其内部引用活得更长。

四、生命周期省略规则

Rust 编译器有三条自动推断规则(生命周期省略规则),如果满足条件则不需要显式标注:

  1. 每个引用参数获得独立的生命周期参数
  2. 如果只有一个输入生命周期参数,它被赋给所有输出生命周期
  3. 如果有多个输入生命周期参数,但其中一个是 &self 或 &mut self,则 self 的生命周期赋给所有输出生命周期

对于复杂场景(如多个返回引用、嵌套引用链),仍然需要手动标注。

五、智能指针:Box、Rc、Arc、Cow

所有权系统的灵活延伸体现在智能指针上:

  • Box<T>:堆分配的独占所有权,用于递归类型和不确定大小类型
  • Rc<T>:引用计数的共享所有权,单线程场景
  • Arc<T>:原子引用计数的共享所有权,多线程安全
  • Cow<'a, T>:写时复制,延迟克隆直到真正需要修改
use std::sync::Arc;
use std::thread;

fn shared_ownership() {
    let data = Arc::new(vec![1, 2, 3, 4, 5]);
    let mut handles = vec![];

    for i in 0..3 {
        let data_clone = Arc::clone(&data);
        handles.push(thread::spawn(move || {
            println!("Thread {}: {:?}", i, data_clone);
        }));
    }

    for h in handles { h.join().unwrap(); }
}

六、内部可变性:RefCell 与 Mutex

Rust 的借用规则在编译期强制执行,但有时需要在运行时检查可变性。内部可变性(Interior Mutability)模式通过 UnsafeCell 提供了这种能力:

  • RefCell<T>:单线程运行时借用检查,违反规则会 panic
  • Mutex<T> / RwLock<T>:多线程互斥访问
  • Cell<T>:基于 Copy 语义的内部可变(无运行时开销)
use std::cell::RefCell;

struct MockMessenger {
    sent_messages: RefCell>,
}

impl MockMessenger {
    fn send(&self, msg: &str) {
        self.sent_messages.borrow_mut().push(msg.to_string());
        // 运行时检查:如果已有不可变借用存在,这里会 panic
    }
}

七、自引用结构与 Pin

自引用结构是 Rust 中最棘手的问题之一。如果结构体内部的字段引用了自己另一个字段,移动结构体会导致内部指针失效。

// 有问题的自引用结构(不安全的)
struct SelfReferential {
    data: String,
    pointer_to_data: *const String, // 指向自己的 data 字段
}

// 正确做法:使用 Pin
use std::pin::Pin;
use std::marker::PhantomPinned;

struct FixedSelfRef {
    data: String,
    pin_to_data: *const String,
    _pin: PhantomPinned, // 禁止 Unpin,确保不会被移动
}

impl FixedSelfRef {
    fn new(data: String) -> Pin> {
        let mut boxed = Box::new(FixedSelfRef {
            data,
            pin_to_data: std::ptr::null(),
            _pin: PhantomPinned,
        });
        let ptr = &boxed.data as *const String;
        unsafe { boxed.as_mut().get_unchecked_mut().pin_to_data = ptr; }
        boxed.into()
    }
}

Pin<P> 的语义是"指针指向的内容不会被移动"。配合 !Unpin(通过 PhantomPinned),保证了自引用结构在内存中的稳定性。async/await 生成的 Future 就大量依赖这一机制——因为 async 函数的局部变量可能在 await 点之间被移动。

八、总结

Rust 的所有权系统是一套精密的编译期内存管理方案。从所有权三原则到借用检查、从生命周期标注到智能指针、从内部可变性到 Pin——它们层层递进,共同构建了 Rust "无畏并发、零成本抽象"的承诺。掌握这些概念,你就能理解 Rust 编译器那些"为什么不让通过"的错误提示,写出既安全又高效的系统级代码。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部