【Rust入门】第三章:所有权 (Ownership) 与借用 (Borrowing)

36 阅读6分钟

现在,请深吸一口气,坐直身体。我们要进入 第三章:所有权 (Ownership) 与借用 (Borrowing)

⚠️ 警告:这是 Rust 学习曲线中最陡峭的部分。

  • 在 C/C++ 中,你需要手动管理内存(malloc/free),容易忘释放(内存泄漏)或释放两次(崩溃)。
  • 在 Java/Python/Go 中,有垃圾回收器(GC)帮你自动回收,但会消耗性能,导致程序偶尔卡顿。
  • Rust 选择了第三条路:通过一套编译时的规则(所有权)来管理内存。编译器非常严格,如果你违反规则,代码根本无法编译通过。

这一章一旦顿悟,你就算正式“入门” Rust 了!

请新建项目:

cargo new ownership_test
cd ownership_test

🚀 第三章:所有权 (Ownership)

3.1 栈 (Stack) 与 堆 (Heap) —— 前置知识

为了理解 Rust 为什么这么做,你需要简单了解内存:

  • 栈 (Stack):像一摞盘子。存取极快,但大小必须固定。比如 i32char[i32; 5] 都存在栈上。
  • 堆 (Heap):像一个杂乱的大仓库。如果你需要存一段文字(String),因为不知道用户会输入多长,所以必须存在堆上。你把数据扔进仓库,管理员给你一张小纸条(指针),上面写着数据存放的位置。你把这张小纸条放在栈上。

3.2 所有权的三大铁律

请务必死记硬背这三条规则:

  1. Rust 中的每一个值都有一个 所有者 (Owner)(变量)。
  2. 同一时间,该值只能有一个所有者。
  3. 当所有者离开作用域(Scope)时,这个值将被丢弃(Drop,即释放内存)。

3.3 变量作用域 (Scope)

这和其他语言类似,用花括号 {} 界定。

fn main() {
    {                      // --- s 在这里开始有效
        let s = "hello";   // s 是 valid 的
        // 使用 s ...
    }                      // --- 作用域结束,s 不再有效
                           // 这里的 s 已经被清理了
}

3.4 移动 (Move) —— Rust 最独特的行为

这是新手最容易懵的地方。

情况 A:栈上的数据(拷贝)

fn main() {
    let x = 5;
    let y = x; // 把 5 拷贝一份给 y
    println!("x = {}, y = {}", x, y);
}

这很正常,xy 都等于 5。因为整数大小固定,都在栈上,拷贝极快。Rust 称这些类型实现了 Copy 特征。

情况 B:堆上的数据(移动) 我们要引入 String 类型。不同于字符串字面量("hello",它是写死在代码里的),String 是动态的,分配在堆上。

请务必运行这段代码,并观察报错:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // <--- 关键步骤!

    // 下面这行会报错!
    println!("s1 = {}", s1); 
}

为什么报错?

  1. s1 在堆上申请了内存存 "hello",并在栈上存了一个指针指向它。
  2. let s2 = s1; 发生时,Rust 并没有 把堆上的 "hello" 复制一份(深拷贝太慢了)。
  3. 它只是把 s1 栈上的指针复制给了 s2
  4. 关键点:为了保证内存安全(避免二次释放),Rust 认为 s1 已经失效了! 这个操作叫 移动 (Move)
  5. 就像现实生活中,我把手里的苹果给了你,我就没有苹果了。

修正:克隆 (Clone) 如果你真的想完整复制一份堆上的数据(即使这比较消耗性能),必须显式调用 .clone()

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone(); // 深拷贝:堆上的数据也被复制了

    println!("s1 = {}, s2 = {}", s1, s2); // 正常运行
}

3.5 所有权与函数

把变量传给函数,也遵循同样的规则:传进去,就没了。

fn main() {
    let s = String::from("hello");  // s 进入作用域

    takes_ownership(s);             // s 的值被【移动】进了函数
                                    // 从这里开始,s 不再有效了!

    // println!("{}", s);           // ❌ 如果取消注释,会报错
    
    let x = 5;                      // x 进入作用域
    makes_copy(x);                  // x 是 i32,实现了 Copy 特征
                                    // 所以 x 只是把值传进去了,后面还能用
    println!("x 依然可用: {}", x); 
} // main 结束

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

fn makes_copy(some_integer: i32) {
    println!("{}", some_integer);
}

痛苦点: 如果我想在调用 takes_ownership 之后继续使用 s 怎么办?难道必须让函数把 sreturn 回来吗?

// 笨办法:把所有权拿进去,再拿出来
fn main() {
    let s1 = String::from("hello");
    let (s2, len) = calculate_length(s1); // s1 移动进去了,s2 移动出来了
    println!("The length of '{}' is {}.", s2, len);
}

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

这样做太麻烦了!于是,Rust 引入了 “引用” (Reference)


🔗 第 3.5 章:引用与借用 (References and Borrowing)

这就是“借用”:我可以使用你的东西,但不获取它的所有权。

1. 不可变引用 (&)

我们在参数类型前面加 &,传参时也加 &

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1); // 传引用,不传所有权

    // s1 依然有效!因为我们只是把它“借”出去了
    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
    s.len()
} // s 离开作用域,但因为它没有所有权,什么也不会发生(不会 drop String)

尝试修改借用的值?

fn change(some_string: &String) {
    some_string.push_str(", world"); // ❌ 报错!
}

规则:借来的书(不可变引用),你是不能在上面乱涂乱画的。

2. 可变引用 (&mut)

如果你想修改,必须使用 可变引用

fn main() {
    let mut s = String::from("hello"); // 变量本身必须是 mut

    change(&mut s); // 传入可变引用

    println!("{}", s); // 输出 hello, world
}

fn change(some_string: &mut String) { // 接收可变引用
    some_string.push_str(", world");
}

3. 借用的终极规则 (重点!)

Rust 为了防止多线程数据竞争,立下了严格的规矩:

在同一时间,对于同一个数据,你只能拥有以下二者之一

  • A. 任意数量的不可变引用 (&T) (大家都可以看书,没人能改)
  • B. 唯一的一个可变引用 (&mut T) (只有一个人能拿回家改,别人都不能看了)

代码实验:违反规则

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; 
    let r2 = &s; 
    // let r3 = &mut s; // ❌ 报错!已经有 r1, r2 在借阅了,不能再借给 r3 去修改

    println!("{}, {}", r1, r2);
}

但是,注意作用域。如果 r1r2 用完了(也就是最后一次使用已经在代码上方了),编译器很聪明,知道后面没人看了,就可以创建可变引用了(这叫 NLL - Non-Lexical Lifetimes)。


📝 第三章总结与作业

这一章非常“烧脑”,但请不要放弃。

总结:

  1. 所有权:每个值有一个所有者,变量赋值给别人,自己就废了(针对堆数据)。
  2. MoveString 赋值是 Move,i32 赋值是 Copy。
  3. 引用 (&):允许你不获取所有权也能访问数据。
  4. 借用规则:要么有很多读者(不可变),要么有一个作者(可变),不能同时存在。

作业:

  1. 创建一个函数 add_suffix,接收一个 &mut String,在字符串后面追加 " - Rust is cool!"。
  2. main 函数中,创建一个 String,调用这个函数,然后打印修改后的字符串。
  3. 思考题(不用写代码):为什么 Rust 不允许同时存在 &s (不可变) 和 &mut s (可变)?如果允许,当我在读取 &s 的时候,另一个人通过 &mut s 吧数据清空了,会发生什么?(这就是 Rust 防止的“数据竞争”)。