所有权与借用¶
📚 章节概述¶
所有权(Ownership)是 Rust 最独特的特性,它让 Rust 无需垃圾回收器就能保证内存安全。本章将深入讲解所有权系统、借用规则、引用和生命周期。
🎯 学习目标¶
- 理解所有权的三大规则
- 掌握借用和引用的概念
- 学会使用生命周期标注
- 理解内存管理机制
- 避免常见的所有权错误
📖 所有权系统¶
1.1 什么是所有权¶
所有权是 Rust 管理内存的核心机制。它通过一套编译时规则来管理内存,无需运行时垃圾回收器。
所有权规则: 1. Rust 中的每个值都有一个所有者(owner) 2. 值在同一时间只能有一个所有者 3. 当所有者离开作用域时,值将被丢弃
1.2 栈和堆¶
理解栈和堆的区别对于掌握所有权至关重要:
fn main() {
// 栈上的数据(固定大小,快速)
let x = 5;
let y = x; // 复制值
// 堆上的数据(动态大小,灵活)
let s1 = String::from("hello");
let s2 = s1; // 移动所有权
// println!("{}", s1); // 编译错误!s1 已被移动
println!("{}", s2);
}
栈 vs 堆:
| 特性 | 栈 | 堆 |
|---|---|---|
| 分配速度 | 快 | 慢 |
| 访问速度 | 快 | 慢 |
| 数据大小 | 固定 | 动态 |
| 用途 | 基本类型、局部变量 | 复杂数据结构 |
1.3 所有权转移¶
移动(Move)¶
fn main() {
let s1 = String::from("hello"); // String::from 创建堆上的字符串
let s2 = s1; // s1 的所有权移动到 s2
// println!("{}", s1); // 编译错误!s1 不再有效
println!("{}", s2);
}
克隆(Clone)¶
如果需要深度复制,使用 clone 方法:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // 深度复制
println!("s1 = {}, s2 = {}", s1, s2);
}
拷贝(Copy)¶
实现了 Copy trait 的类型在赋值时会自动拷贝:
fn main() {
let x = 5;
let y = x; // 拷贝,x 仍然有效
println!("x = {}, y = {}", x, y);
let s1 = String::from("hello");
let s2 = s1; // 移动,s1 不再有效
// println!("{}", s1); // 编译错误
}
实现 Copy 的类型: - 所有整数类型 - 布尔类型 - 浮点类型 - 字符类型 - 元组(如果所有元素都实现了 Copy)
🔗 函数与所有权¶
2.1 传递值给函数¶
fn main() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // s 的所有权移动到函数
// println!("{}", s); // 编译错误!s 不再有效
let x = 5; // x 进入作用域
makes_copy(x); // x 拷贝到函数
println!("{}", x); // x 仍然有效
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // some_string 离开作用域,`drop` 被调用,内存被释放
fn makes_copy(some_integer: i32) {
println!("{}", some_integer);
} // some_integer 离开作用域,但因为是 Copy 类型,不需要特殊处理
2.2 返回值与作用域¶
fn main() {
let s1 = gives_ownership(); // gives_ownership 将返回值的所有权移动给 s1
let s2 = String::from("hello"); // s2 进入作用域
let s3 = takes_and_gives_back(s2); // s2 移动到函数,函数返回值移动给 s3
// println!("{}", s2); // 编译错误!s2 不再有效
} // s1、s3 离开作用域,被 drop
fn gives_ownership() -> String {
let some_string = String::from("yours");
some_string // some_string 的所有权被返回
}
fn takes_and_gives_back(a_string: String) -> String {
a_string // a_string 的所有权被返回
}
🔍 引用与借用¶
3.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.len()
} // s 离开作用域,但因为它不拥有引用的值,所以什么也不会发生
3.2 可变引用¶
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
可变引用的限制: 1. 在特定作用域中,一个数据只能有一个可变引用 2. 不能同时拥有可变和不可变引用
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
println!("{} and {}", r1, r2);
// r1 和 r2 不再使用
let r3 = &mut s; // 没问题
println!("{}", r3);
}
3.3 悬垂引用¶
Rust 编译器防止悬垂引用:
fn main() {
let reference_to_nothing = dangle();
}
// 编译错误!
// fn dangle() -> &String {
// let s = String::from("hello");
// &s // 返回 s 的引用
// } // s 离开作用域并被丢弃,引用指向无效内存
// 正确的做法
fn dangle() -> String {
let s = String::from("hello");
s // 返回 s 的所有权
}
📏 切片¶
4.1 字符串切片¶
fn main() {
let s = String::from("hello world");
let len = s.len();
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"
println!("hello: {}, world: {}", hello, world);
// 简写
let slice = &s[0..2]; // "he"
let slice = &s[..2]; // "he"
let slice = &s[3..len]; // "lo world"
let slice = &s[3..]; // "lo world"
let slice = &s[0..len]; // "hello world"
let slice = &s[..]; // "hello world"
}
4.2 字符串字面量是切片¶
4.3 其他切片¶
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // 类型是 &[i32]
println!("slice: {:?}", slice);
}
⏰ 生命周期¶
5.1 生命周期标注¶
生命周期标注确保引用始终有效:
fn main() {
let string1 = String::from("long string is long");
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { // 'a 生命周期标注,确保引用有效
if x.len() > y.len() {
x
} else {
y
}
}
注意: 返回值的生命周期必须与某个输入参数的生命周期相关联。以下示例会编译失败,因为result可能引用了已失效的string2:
// 这段代码无法编译!
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
} // string2 在这里被丢弃
// println!("The longest string is {}", result); // 编译错误!
}
5.2 结构体中的生命周期¶
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
println!("Important excerpt: {}", i.part);
}
5.3 生命周期省略规则¶
Rust 编译器有三条省略规则,让我们在大多数情况下无需手动标注生命周期:
规则一:每个引用参数都获得各自的生命周期参数
// 编写时省略生命周期:
fn print_str(s: &str) { println!("{}", s); }
// 编译器补全后等价于:
// fn print_str<'a>(s: &'a str) { println!("{}", s); }
// 两个参数各自获得独立的生命周期:
fn longer(a: &str, b: &str) { /* ... */ }
// 编译器补全后等价于:
// fn longer<'a, 'b>(a: &'a str, b: &'b str) { /* ... */ }
规则二:如果只有一个输入生命周期参数,它被赋予所有输出生命周期参数
// 编写时省略生命周期:
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 first_word<'a>(s: &'a str) -> &'a str { ... }
// 只有一个输入引用,所以返回值自动使用相同的生命周期
规则三:如果有多个输入生命周期参数,但其中一个是 &self 或 &mut self,self 的生命周期被赋予所有输出生命周期参数
struct Parser {
input: String,
}
impl Parser {
// 编写时省略生命周期:
fn parse(&self, prefix: &str) -> &str {
&self.input[prefix.len()..]
}
// 编译器补全后等价于:
// fn parse<'a, 'b>(&'a self, prefix: &'b str) -> &'a str { ... }
// 返回值的生命周期跟随 &self,而非 prefix
}
注意:当三条规则都无法确定返回值的生命周期时,编译器会报错,要求你手动标注:
// 编译错误!两个输入引用且没有 &self,编译器无法推断返回值生命周期
// fn longest(x: &str, y: &str) -> &str { ... }
// 必须手动标注:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
5.4 静态生命周期¶
'static 生命周期表示引用在整个程序运行期间都有效:
📝 练习题¶
练习 1:所有权转移¶
fn main() {
let s1 = String::from("hello");
let s2 = s1;
// TODO: 修复代码,使其能编译通过
println!("{}", s1);
}
练习 2:函数返回值¶
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len);
}
// TODO: 实现 calculate_length 函数
fn calculate_length(s: String) -> (String, usize) {
// 返回字符串和其长度
todo!()
}
练习 3:借用规则¶
fn main() {
let mut s = String::from("hello");
// TODO: 修复代码,使其能编译通过
let r1 = &s;
let r2 = &mut s;
println!("{} and {}", r1, r2);
}
练习 4:生命周期¶
fn main() {
let string1 = String::from("long string is long");
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {}", result);
}
// TODO: 添加生命周期标注
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
💡 最佳实践¶
1. 优先使用不可变引用¶
// 好
fn read_data(data: &Data) {
// 只读取数据
}
// 避免不必要的可变引用
fn read_data(data: &mut Data) {
// 如果不需要修改,不要使用可变引用
}
2. 最小化借用范围¶
// 好
fn process(data: &mut Data) {
let temp = data.calculate_temp();
data.update(temp);
}
// 避免
fn process(data: &mut Data) {
let temp = data.calculate_temp();
let other = data.calculate_other();
data.update(temp);
data.update_other(other);
}
3. 使用 Cow 避免不必要的克隆¶
use std::borrow::Cow;
fn process_string(s: &str) -> Cow<str> {
if s.contains("special") {
// 需要修改,返回拥有的 String
let mut result = s.to_string();
result.push_str("_processed");
Cow::Owned(result)
} else {
// 不需要修改,返回借用
Cow::Borrowed(s)
}
}
⚠️ 常见错误¶
1. 使用已移动的值¶
// 错误
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); // 编译错误
// 正确
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{}", s1);
2. 多个可变引用¶
// 错误
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // 编译错误
// 正确
let mut s = String::from("hello");
{
let r1 = &mut s;
println!("{}", r1);
}
let r2 = &mut s;
println!("{}", r2);
3. 生命周期不匹配¶
// 错误
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
if x.len() > y.len() {
x
} else {
y // 编译错误!y 的生命周期可能比 'a 短
}
}
// 正确
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
📚 扩展阅读¶
🎯 本章小结¶
本章深入讲解了 Rust 的所有权系统:
- ✅ 理解所有权的三大规则
- ✅ 掌握移动、拷贝和克隆的区别
- ✅ 学会使用不可变和可变引用
- ✅ 理解借用规则和悬垂引用
- ✅ 掌握字符串切片
- ✅ 理解生命周期标注和省略规则
下一章: 我们将学习结构体与枚举,了解如何定义自定义数据类型。