第8章 - 所有权
嗨,朋友!我是长安。
终于来到这一章了!所有权(Ownership)是 Rust 最核心、最独特的概念,也是 Rust 能保证内存安全的秘密武器。
我知道你可能听说过"所有权很难理解",但别担心,我会用最简单的方式,用生活中的例子来解释给你听。
理解了所有权,你就理解了 Rust 的精髓!
🤔 为什么需要所有权?
在讲所有权之前,我们先理解一个问题:内存管理。
不同语言的内存管理方式
Python、Java、JavaScript:
- 使用垃圾回收器(Garbage Collector, GC)
- 程序运行时会自动清理不用的内存
- 优点:程序员不用操心
- 缺点:GC 会占用性能,有时会导致程序暂停
C、C++:
- 手动管理内存
- 程序员要自己分配(
malloc)和释放(free)内存 - 优点:性能高,完全控制
- 缺点:容易出错(忘记释放、重复释放、悬垂指针等)
Rust:
- 通过所有权系统在编译时管理内存
- 优点:既安全又高效,没有 GC,没有手动管理的麻烦
- 缺点:需要学习新概念(但非常值得!)
长安说
想象一下:
- Python/Java 就像住酒店,有人帮你打扫房间(GC),但你要付钱(性能开销)
- C/C++ 就像自己租房,自己打扫,但可能会忘记倒垃圾(内存泄漏)
- Rust 就像有个智能机器人(编译器),在你入住前就检查你的打扫计划是否合理,确保你不会忘记任何事情!
📖 所有权三大规则
Rust 的所有权系统只有三条简单的规则:
- 每个值都有一个所有者(owner)
- 一个值同时只能有一个所有者
- 当所有者离开作用域,值会被自动清理
就这么简单!让我们逐条理解。
🎯 规则1:每个值都有一个所有者
fn main() {
let s = String::from("hello"); // s 是这个字符串的所有者
}
这里:
String::from("hello")创建了一个字符串s成为了这个字符串的所有者
长安说
所有者就像是这个值的"主人"。你可以把值想象成一只宠物狗,s 就是狗的主人。
🔒 规则2:一个值同时只能有一个所有者
这是最重要的规则!看这个例子:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // 所有权从 s1 转移到了 s2
println!("{}", s1); // ❌ 错误!s1 不再有效
}
编译器会报错:
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:20
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{}", s1);
| ^^ value borrowed here after move
发生了什么?
当你写 let s2 = s1 时,所有权从 s1 转移(move)到了 s2。此时:
s2成为了字符串的新主人s1不再有效,不能再使用
长安提醒
这和其他语言非常不同!在 Python 或 Java 中,这种赋值会创建一个引用,两个变量都能用。但在 Rust 中,所有权会转移。
为什么要这样设计?
想象一下,如果两个变量都能拥有同一块内存:
- 两个变量都离开作用域
- 两个都尝试释放这块内存
- 双重释放(double free)错误!程序崩溃或安全漏洞
Rust 通过所有权规则,在编译时就杜绝了这个问题!
生活例子
长安说
想象你有一本书(值),一开始你是主人(s1)。
你把书送给了朋友(let s2 = s1),现在朋友是主人(s2)。
你(s1)就不能再看这本书了,因为书已经不属于你了!
Rust 的规则就是这么直接:一个东西只能有一个主人。
🔄 如何保留原始变量?
如果你想让 s1 继续有效,有两个办法:
方法1:克隆(Clone)
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // 深拷贝,创建一个副本
println!("s1 = {}", s1); // ✅ s1 依然有效
println!("s2 = {}", s2); // ✅ s2 也有效
}
clone() 会创建一个完整的副本,s1 和 s2 各自拥有自己的数据。
注意
克隆会复制数据,如果数据很大,可能会比较慢。只在真正需要两份数据时才用。
方法2:使用引用(下一章会详细讲)
fn main() {
let s1 = String::from("hello");
let s2 = &s1; // 借用,不转移所有权
println!("s1 = {}", s1); // ✅ 都有效
println!("s2 = {}", s2); // ✅
}
📦 栈(Stack)vs 堆(Heap)
为什么 i32 类型不会移动所有权,而 String 会呢?
fn main() {
let x = 5;
let y = x; // 没有移动所有权!
println!("x = {}", x); // ✅ x 依然有效
println!("y = {}", y); // ✅ y 也有效
}
这是因为 Rust 区分了两种数据存储方式:
栈(Stack)
- 存储固定大小的数据
- 非常快
- 数据在编译时大小已知
存储在栈上的类型:
- 整数:
i32,u64等 - 浮点数:
f32,f64 - 布尔值:
bool - 字符:
char - 元组(如果元素都在栈上)
这些类型实现了 Copy trait,赋值时会自动复制,不会移动所有权。
堆(Heap)
- 存储大小不固定的数据
- 相对较慢
- 需要在运行时分配内存
存储在堆上的类型:
StringVec<T>(向量)Box<T>(智能指针)
这些类型赋值时会移动所有权。
长安说
为什么区别对待?
- 栈上的数据很小(比如一个整数只有 4 字节),复制很快,所以就直接复制
- 堆上的数据可能很大(字符串可能有几 MB),复制很慢,所以用移动所有权的方式,避免不必要的复制
🚪 规则3:离开作用域,值被清理
fn main() {
{
let s = String::from("hello"); // s 进入作用域
println!("{}", s); // s 在这里有效
} // s 离开作用域,内存被自动释放
// println!("{}", s); // ❌ 错误!s 已经不存在了
}
当变量离开它的作用域(scope,就是那对大括号 {})时,Rust 会自动调用 drop 函数清理内存。
长安说
这就像你下班回家,离开办公室时,灯会自动关掉。你不需要手动关灯,Rust 会帮你做!
📝 函数与所有权
函数参数会转移所有权
fn main() {
let s = String::from("hello");
takes_ownership(s); // s 的所有权转移给函数
// println!("{}", s); // ❌ 错误!s 已经无效了
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // some_string 离开作用域,内存被释放
函数返回值会转移所有权
fn main() {
let s1 = gives_ownership(); // 函数返回值的所有权转移给 s1
println!("{}", s1); // ✅ 可以使用
}
fn gives_ownership() -> String {
let some_string = String::from("hello");
some_string // 返回,所有权转移给调用者
}
实际应用:计算字符串长度
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("字符串 '{}' 的长度是 {}", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length) // 把 s 的所有权返回,避免被释放
}
但这样写很麻烦!每次都要把所有权转来转去。
更好的方法:使用引用(下一章会讲)
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // 传递引用,不转移所有权
println!("字符串 '{}' 的长度是 {}", s1, len); // s1 依然有效!
}
fn calculate_length(s: &String) -> usize {
s.len()
}
💡 小结
在这一章,我们学会了 Rust 最核心的概念——所有权:
三大规则:
- 每个值都有一个所有者
- 一个值同时只能有一个所有者
- 当所有者离开作用域,值会被自动清理
核心要点:
- 所有权会转移(move),转移后原变量失效
- 栈上的数据(如
i32)会自动复制,不会转移所有权 - 堆上的数据(如
String)会转移所有权 - 函数参数和返回值也会转移所有权
- 可以用
clone()创建副本,或者用引用(下一章)
长安的建议
所有权是 Rust 的核心,一开始可能会觉得很繁琐,经常跟编译器"打架"。但相信我,一旦理解了,你会发现:
- 你写的代码几乎没有内存 bug
- 你对内存管理有了更深的理解
- 这些知识对学习其他语言也很有帮助
如果你现在还没完全理解,没关系! 继续往下学,多写代码,慢慢就会有"顿悟"的时刻。
🚀 下一步
所有权的概念很强大,但每次都转移所有权会很麻烦。
下一章,我们会学习引用与借用,这样你就可以"借用"数据,而不需要转移所有权了!
💪 练习题
用代码验证这些概念:
- 创建一个字符串,赋值给另一个变量,尝试打印原变量,观察编译错误
- 使用
clone()复制一个字符串,确认两个变量都能使用 - 创建一个函数,接收一个
String参数,打印它,然后在main中调用这个函数,观察所有权转移 - 创建一个整数,赋值给另一个变量,确认两个都能打印(体验
Copytrait)
答案示例
fn main() {
// 练习1
let s1 = String::from("hello");
let s2 = s1;
// println!("{}", s1); // 编译错误
// 练习2
let s3 = String::from("world");
let s4 = s3.clone();
println!("s3 = {}, s4 = {}", s3, s4); // 都能用
// 练习3
let s5 = String::from("rust");
print_string(s5);
// println!("{}", s5); // 编译错误,所有权已转移
// 练习4
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y); // 都能用,因为 i32 实现了 Copy
}
fn print_string(s: String) {
println!("{}", s);
}
