第06章:所有权系统
第06章:所有权系统
所有权系统是 Rust 最核心、最独特的特性。理解所有权是学好 Rust 的关键。
6.1 什么是所有权
栈与堆
| 特性 | 栈(Stack) | 堆(Heap) |
|---|---|---|
| 分配速度 | 极快(移动栈指针) | 较慢(需查找空闲空间) |
| 大小 | 编译时确定 | 运行时确定 |
| 生命周期 | 作用域结束自动释放 | 需要手动或自动管理 |
| 典型数据 | 整数、浮点、布尔、指针 | String、Vec、Box |
所有权三条规则
fn main() {
// 规则1:每个值有且仅有一个所有者
let s = String::from("hello");
// 规则2:同一时间只能有一个所有者
let s2 = s; // s 的所有权移动(move)到 s2
// println!("{}", s); // ❌ s 已失效
println!("{}", s2); // ✅ s2 是当前所有者
// 规则3:当所有者离开作用域,值被自动释放
} // s2 离开作用域,String 被释放(调用 drop)
6.2 移动语义(Move)
String 的移动
fn main() {
let s1 = String::from("hello");
let s2 = s1; // 移动(move)
// s1 不再有效,不能再使用
// println!("{}", s1); // ❌ 编译错误
println!("{}", s2); // ✅
}
内存图示:
移动前:
s1 → [ptr|len|cap] → 堆: "hello"
s2 (未初始化)
移动后:
s1 (已失效)
s2 → [ptr|len|cap] → 堆: "hello"
Copy 类型(复制而非移动)
实现了 Copy trait 的类型在赋值时会复制而非移动:
fn main() {
let x = 5;
let y = x; // 复制(copy)
// 两个都有效
println!("x={}, y={}", x, y); // ✅
// Copy 类型包括:整数、浮点、布尔、字符、只包含 Copy 类型的元组
}
| 类型 | 移动/复制 | 说明 |
|---|---|---|
i32, f64, bool, char | Copy | 栈上数据,复制成本低 |
(i32, f64) | Copy | 只含 Copy 类型的元组 |
String | Move | 堆上数据,移动避免双重释放 |
Vec<T> | Move | 堆上数据 |
&T, &mut T | Copy | 引用本身是复制的 |
Box<T> | Move | 堆上数据 |
6.3 克隆(Clone)
如果需要深拷贝堆上数据,使用 clone():
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // 深拷贝
// 两个都有效且独立
println!("s1={}, s2={}", s1, s2); // ✅
// 修改 s2 不影响 s1
let mut s2 = s2;
s2.push_str(" world");
println!("s1={}, s2={}", s1, s2); // s1="hello", s2="hello world"
}
注意:
clone()可能很昂贵(需要分配内存并复制数据),只在确实需要独立副本时使用。
6.4 函数与所有权
传递所有权
fn takes_ownership(s: String) {
println!("获得所有权: {}", s);
} // s 被释放
fn makes_copy(n: i32) {
println!("复制的值: {}", n);
} // n 被释放,但原始值不受影响
fn main() {
let s = String::from("hello");
takes_ownership(s);
// println!("{}", s); // ❌ s 的所有权已移动到函数中
let x = 5;
makes_copy(x);
println!("x 仍然有效: {}", x); // ✅ x 是 Copy 类型
}
返回所有权
fn gives_ownership() -> String {
let s = String::from("hello");
s // 返回时移动所有权
}
fn takes_and_gives_back(s: String) -> String {
println!("临时获得: {}", s);
s // 返回时归还所有权
}
fn main() {
let s1 = gives_ownership();
println!("s1 = {}", s1);
let s2 = takes_and_gives_back(s1);
// println!("{}", s1); // ❌ s1 已移动
println!("s2 = {}", s2); // ✅
}
6.5 引用与借用(Reference & Borrowing)
不可变引用(&T)
fn calculate_length(s: &String) -> usize {
s.len()
} // s 是引用,离开作用域不会释放原始数据
fn main() {
let s = String::from("hello");
let len = calculate_length(&s); // 借用 s
println!("\"{}\" 的长度是 {}", s, len); // ✅ s 仍然有效
}
可变引用(&mut T)
fn append_world(s: &mut String) {
s.push_str(", world!");
}
fn main() {
let mut s = String::from("hello");
append_world(&mut s); // 可变借用
println!("{}", s); // hello, world!
}
借用规则详解
fn main() {
let mut s = String::from("hello");
// ✅ 多个不可变引用可以共存
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);
// ✅ 不可变引用使用完后,可以创建可变引用
let r3 = &mut s;
r3.push_str(" world");
println!("{}", r3);
// ❌ 不能同时有不可变引用和可变引用
// let r4 = &s;
// let r5 = &mut s;
// println!("{} {}", r4, r5); // 编译错误
}
非词法生命周期(NLL)
Rust 2018+ 引入 NLL,引用的生命周期在最后一次使用时结束:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);
// r1 和 r2 在此之后不再使用(NLL),生命周期结束
let r3 = &mut s; // ✅ 此时可以创建可变引用
r3.push_str(" world");
println!("{}", r3);
}
6.6 悬垂引用(Dangling Reference)
Rust 编译器禁止悬垂引用:
// ❌ 编译错误:返回对局部变量的引用
// fn dangle() -> &String {
// let s = String::from("hello");
// &s // s 在函数结束时被释放
// }
// ✅ 正确做法:返回所有权
fn no_dangle() -> String {
let s = String::from("hello");
s // 移动所有权给调用者
}
fn main() {
let s = no_dangle();
println!("{}", s);
}
6.7 生命周期(Lifetime)
为什么需要生命周期
// ❌ 编译器不知道返回值的生命周期与哪个参数相关
// fn longest(x: &str, y: &str) -> &str {
// if x.len() > y.len() { x } else { y }
// }
// ✅ 使用生命周期标注
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("long string");
let result;
{
let s2 = String::from("xyz");
result = longest(s1.as_str(), s2.as_str());
println!("最长的字符串: {}", result);
}
// println!("{}", result); // ❌ 可能编译错误:s2 已释放
}
生命周期语法
| 语法 | 说明 |
|---|---|
'a | 生命周期参数名称(通常用小写字母) |
&'a T | 带生命周期的不可变引用 |
&'a mut T | 带生命周期的可变引用 |
T: 'a | T 中的所有引用至少活 'a 那么久 |
结构体中的生命周期
#[derive(Debug)]
struct Excerpt<'a> {
text: &'a str,
}
impl<'a> Excerpt<'a> {
fn level(&self) -> i32 {
3
}
fn announce(&self, announcement: &str) -> &str {
println!("注意: {}", announcement);
self.text // 返回的生命周期与 self 相同
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence;
{
let i = novel.find('.').unwrap_or(novel.len());
first_sentence = Excerpt {
text: &novel[..i],
};
}
println!("摘录: {:?}", first_sentence);
}
生命周期省略规则
编译器会自动推断生命周期,不需要总是手动标注。三条规则:
- 每个引用参数获得各自的生命周期
- 如果只有一个输入生命周期,该生命周期赋给所有输出引用
- 如果方法有
&self或&mut self,self的生命周期赋给所有输出引用
// 不需要标注的情况
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &s[..i];
}
}
&s[..]
}
// 等价于标注后
// fn first_word<'a>(s: &'a str) -> &'a str { ... }
fn main() {
let s = String::from("hello world");
let word = first_word(&s);
println!("第一个单词: {}", word);
}
‘static 生命周期
// 'static 生命周期的数据存在于整个程序运行期间
let s: &'static str = "我有静态生命周期";
fn main() {
// 字符串字面量都是 'static
let greeting: &'static str = "hello";
println!("{}", greeting);
}
6.8 同时使用泛型、trait bound 和生命周期
use std::fmt::Display;
fn longest_with_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("公告: {}", ann);
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("long string");
let s2 = "xyz";
let result = longest_with_announcement(
s1.as_str(),
s2,
"正在比较两个字符串的长度!",
);
println!("最长: {}", result);
}
6.9 业务场景示例
缓存系统(所有权视角)
use std::collections::HashMap;
struct Cache {
data: HashMap<String, String>,
max_size: usize,
}
impl Cache {
fn new(max_size: usize) -> Self {
Self {
data: HashMap::new(),
max_size,
}
}
// 获取值的引用(借用)
fn get(&self, key: &str) -> Option<&String> {
self.data.get(key)
}
// 插入值(转移所有权)
fn set(&mut self, key: String, value: String) {
if self.data.len() >= self.max_size {
// 移除一个旧条目(释放所有权)
if let Some(oldest_key) = self.data.keys().next().cloned() {
self.data.remove(&oldest_key);
}
}
self.data.insert(key, value);
}
// 获取大小
fn len(&self) -> usize {
self.data.len()
}
}
fn main() {
let mut cache = Cache::new(3);
// set 获得 key 和 value 的所有权
cache.set("user:1".to_string(), "Alice".to_string());
cache.set("user:2".to_string(), "Bob".to_string());
cache.set("user:3".to_string(), "Charlie".to_string());
// get 返回引用(借用)
if let Some(name) = cache.get("user:1") {
println!("用户1: {}", name);
}
// 添加第4个条目,会驱逐一个旧条目
cache.set("user:4".to_string(), "David".to_string());
println!("缓存大小: {}", cache.len()); // 3
}
文本处理器
/// 统计文本中每个单词的出现次数
/// 参数是不可变引用,不会夺走调用者的数据
fn word_frequency(text: &str) -> Vec<(&str, usize)> {
let mut freq = std::collections::HashMap::new();
for word in text.split_whitespace() {
// entry API 返回可变引用
*freq.entry(word).or_insert(0) += 1;
}
let mut result: Vec<_> = freq.into_iter().collect();
result.sort_by(|a, b| b.1.cmp(&a.1));
result
}
/// 提取文本摘要(前 n 个句子)
/// 返回的是对原始文本的切片引用
fn summarize(text: &str, n: usize) -> &str {
let mut count = 0;
for (i, ch) in text.char_indices() {
if ch == '.' || ch == '。' {
count += 1;
if count == n {
return &text[..=i];
}
}
}
text // 不足 n 句则返回全部
}
fn main() {
let article = "Rust is a systems programming language. \
Rust is fast and safe. Rust has ownership. \
Rust prevents data races. Rust is loved.";
// 借用 article,不转移所有权
println!("摘要: {}", summarize(article, 2));
println!();
let freq = word_frequency(article);
println!("词频统计:");
for (word, count) in freq.iter().take(5) {
println!(" \"{}\": {}次", word, count);
}
// article 仍然可用
println!("\n原文长度: {} 字节", article.len());
}
6.10 常见错误与解决方案
错误1:在借用期间修改
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
// ❌ 在迭代(借用)期间修改
// for &item in &v {
// if item == 3 {
// v.push(6); // 不能在不可变借用期间可变借用
// }
// }
// ✅ 方案1:收集要修改的信息,迭代后再修改
let should_add = v.contains(&3);
if should_add {
v.push(6);
}
println!("{:?}", v);
// ✅ 方案2:使用 retain 等迭代器方法
let mut v2 = vec![1, 2, 3, 4, 5, 6];
v2.retain(|&x| x != 3); // 移除所有等于3的元素
println!("retain后: {:?}", v2);
}
错误2:返回局部变量的引用
// ❌ 悬垂引用
// fn create_string() -> &str {
// let s = String::from("hello");
// &s // s 在函数结束后被释放
// }
// ✅ 返回所有权
fn create_string() -> String {
String::from("hello")
}
// ✅ 返回 'static 引用
fn create_static() -> &'static str {
"hello" // 字面量有 'static 生命周期
}
fn main() {
let s1 = create_string();
let s2 = create_static();
println!("{}, {}", s1, s2);
}
错误3:闭包捕获所有权
fn main() {
let name = String::from("Alice");
// move 闭包获得所有权
let closure = move || {
println!("Hello, {}", name);
};
closure();
// println!("{}", name); // ❌ name 已移动到闭包中
// 如果是 Copy 类型则无此问题
let n = 42;
let closure2 = move || println!("n = {}", n);
closure2();
println!("n 仍然有效: {}", n); // ✅ i32 是 Copy 类型
}
6.11 本章小结
| 要点 | 说明 |
|---|---|
| 所有权 | 每个值有且仅有一个所有者,离开作用域自动释放 |
| 移动 | 非 Copy 类型赋值时所有权移动,原变量失效 |
| 克隆 | clone() 创建深拷贝,独立副本 |
| 不可变引用 | &T,可有多个,只读访问 |
| 可变引用 | &mut T,同一时间只能有一个 |
| 生命周期 | 编译器确保引用始终有效,不出现悬垂引用 |
| NLL | 引用生命周期在最后一次使用时结束 |
| 省略规则 | 编译器可自动推断大部分生命周期 |
扩展阅读
- The Rust Book - 所有权 — 官方教程
- The Rust Book - 生命周期 — 生命周期详解
- Rustonomicon — 深入 Rust 内部机制
- Visualizing memory layout — Rust 内存布局可视化