Rust 所有权系统:核心概念与实战示例

Rust 所有权系统:核心概念与实战示例

理解 Rust 最独特的内存管理方式


为什么需要所有权?

传统编程语言的内存管理方式:

  • 垃圾回收(GC):运行时开销,不可控的暂停
  • 手动管理(malloc/free):容易出错,产生内存泄漏或野指针

Rust 的所有权系统在编译时解决这些问题,无需 GC,也没有运行时开销。


核心规则

  1. 每个值有且只有一个所有者
  2. 所有权可以转移(Move)
  3. 所有者离开作用域时,值被自动释放

实战示例

1. 所有权转移(Move)

rust 复制代码
fn main() {
    // String 类型数据存储在堆上
    let s1 = String::from("hello");
    let s2 = s1; // s1 的所有权移动到 s2
    
    // println!("{}", s1); // ❌ 编译错误:s1 已失效
    println!("{}", s2); // ✅ 正常输出:hello
}

内存示意图:

复制代码
s1 (失效) ──┐
             ├──→ 堆内存 ["hello"]
s2 (所有者) ─┘

2. Copy 类型(栈上复制)

对于在栈(Stack)上存储的、大小已知的简单类型(如整数、布尔值等),赋值时会在栈上完整复制一份数据,而不是"移动"。这些类型实现了 Copy trait。

rust 复制代码
fn main() {
    // 整数类型实现了 Copy trait
    let x = 42;
    let y = x; // 完整复制,x 仍然有效
    
    println!("x = {}, y = {}", x, y); // ✅ 两个都能用
}

常见 Copy 类型:

  • 所有整数类型:i32, u64, usize
  • 布尔类型:bool
  • 浮点类型:f32, f64
  • 字符类型:char
  • 包含上述类型的元组:(i32, bool)

3. 不可变引用(借用)

rust 复制代码
fn main() {
    let s = String::from("Rust");
    let len = calculate_length(&s); // 借用 s
    
    println!("'{}' 的长度是 {}", s, len); // s 仍然有效
}

fn calculate_length(s: &String) -> usize {
    s.len()
    // s 是引用,离开作用域不会释放数据
}

特点:

  • 可以同时存在多个不可变引用
  • 只能读取,不能修改

4. 可变引用

rust 复制代码
fn main() {
    let mut s = String::from("hello");
    change(&mut s); // 传递可变引用
    println!("{}", s); // hello, world
}

fn change(s: &mut String) {
    s.push_str(", world"); // 可以修改数据
}

重要限制: 同一作用域内,对同一数据不能同时有可变引用和不可变引用。

rust 复制代码
fn main() {
    let mut s = String::from("hello");
    
    let r1 = &s;      // 不可变引用
    let r2 = &s;      // 不可变引用,允许
    // let r3 = &mut s; // ❌ 编译错误:不能同时存在不可变和可变引用
    
    println!("{} and {}", r1, r2);
    // r1, r2 不再使用后,可以创建可变引用
    let r3 = &mut s; // ✅ 现在可以了
}

5. 函数参数的所有权转移

rust 复制代码
fn main() {
    let s = String::from("hello");
    take_ownership(s); // s 的所有权移入函数
    
    // println!("{}", s); // ❌ 编译错误:s 已失效
    
    let x = 5;
    make_copy(x); // x 是 Copy 类型,仍然有效
    println!("x = {}", x); // ✅ 正常输出
}

fn take_ownership(some_string: String) {
    println!("{}", some_string);
    // some_string 离开作用域,内存被释放
}

fn make_copy(some_number: i32) {
    println!("{}", some_number);
}

6. 返回值与所有权

rust 复制代码
fn main() {
    let s1 = gives_ownership(); // 函数返回值的所有权给 s1
    let s2 = String::from("hello");
    let s3 = takes_and_gives_back(s2); // s2 移入,返回值移给 s3
    
    println!("{} {}", s1, s3);
    // println!("{}", s2); // ❌ s2 已失效
}

fn gives_ownership() -> String {
    let some_string = String::from("yours");
    some_string // 返回,所有权移出
}

fn takes_and_gives_back(a_string: String) -> String {
    a_string // 返回,所有权移出
}

7. 使用元组返回多个值

rust 复制代码
fn main() {
    let s1 = String::from("hello");
    let (s2, len) = calculate_length(s1);
    
    println!("'{}' 的长度是 {}", s2, len);
    // s1 已失效,s2 拥有数据
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length) // 返回 String 和长度
}

更好的做法: 使用引用避免所有权转移。

rust 复制代码
fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1); // 使用引用
    
    println!("'{}' 的长度是 {}", s1, len); // s1 仍然有效
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

8. 切片:没有所有权的引用

切片允许你引用集合中的一部分,而不获取所有权。

rust 复制代码
fn main() {
    let s = String::from("hello world");
    let hello = &s[0..5];    // 不可变引用,指向 "hello"
    let world = &s[6..11];   // 不可变引用,指向 "world"
    
    println!("{} {}", hello, world); // hello world
}

字符串切片作为参数:

rust 复制代码
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}

fn main() {
    let my_string = String::from("hello world");
    let word = first_word(&my_string[..]); // 传入切片
    println!("第一个词: {}", word);
    
    let my_string_literal = "hello world";
    let word = first_word(my_string_literal); // 传入字符串字面量
    println!("第一个词: {}", word);
}

生命周期标注示例

当函数返回引用时,需要标注生命周期:

rust 复制代码
// 'a 表示 x 和 y 必须至少活的一样长
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(&string1, &string2);
        println!("最长的: {}", result); // ✅ string2 还在作用域内
    }
    // println!("{}", result); // ❌ 编译错误:string2 已失效
}

常见陷阱与解决

陷阱1:在循环中可变借用

rust 复制代码
let mut data = vec![1, 2, 3];
for item in &data { // 不可变借用
    // data.push(4); // ❌ 不能在不可变借用时修改
}

解决: 使用索引或克隆数据。

陷阱2:自引用结构

rust 复制代码
// ❌ 无法直接创建自引用结构
struct SelfRef {
    data: String,
    ptr: &String, // 引用 data,但生命周期问题
}

解决: 使用 Pin 或索引(如存储 usize 偏移量)。

陷阱3:返回局部变量的引用

rust 复制代码
// ❌ 编译错误
fn dangle() -> &String {
    let s = String::from("hello");
    &s // s 离开作用域后被释放,引用无效
}

// ✅ 返回 String 本身
fn no_dangle() -> String {
    let s = String::from("hello");
    s // 所有权移出
}

快速记忆口诀

移动转移所有权,Copy 复制栈数据

借用引用不拥有,可变引用唯一独占

生命周期保安全,编译器检查很严格


总结

操作 所有权变化 适用场景
赋值 let b = a 移动(堆类型)/ 复制(栈类型) 数据转移或复制
传递参数 func(a) 移动或借用 根据函数签名决定
引用 &a 借用,不转移 只读访问
可变引用 &mut a 借用,不转移 读写访问
切片 &a[0..n] 借用部分数据 无需完整所有权

Rust 的所有权系统在编译时保证内存安全,理解这些概念后,你会发现很多运行时错误在编译阶段就被避免了。多练习、多关注编译器的提示,是掌握所有权的最佳途径!


有任何疑问或想深入了解某个概念,欢迎在评论区讨论! 🦀