4.2 借用与引用
在上一节中,我们学习了 Rust 所有权的基本规则:每个值只有一个所有者,当所有者离开作用域时,值会被自动清理。这种设计虽然保证了内存安全,但在实际编程中,我们经常需要在不转移所有权的情况下访问数据。例如,你可能只想“查看”一个变量的值,而不想拥有它。为了实现这一点,Rust 引入了借用(Borrowing) 的概念,而借用的具体实现就是引用(Reference)。
什么是引用?
引用是 Rust 中一种特殊的指针类型,它允许你访问某个值,但并不拥有该值。你可以把引用想象成图书馆的“借书证”:有了借书证,你可以阅读这本书,但书的真正所有权仍然属于图书馆。当借书证过期(引用离开作用域)时,书依然在图书馆里,不会被销毁。
在语法上,创建一个引用使用 & 符号。例如:
let s1 = String::from("hello");
let len = calculate_length(&s1); // 传递 s1 的引用,而不是 s1 本身
fn calculate_length(s: &String) -> usize {
s.len()
} // 这里,s 离开了作用域,但由于它只是引用,所以它指向的 String 不会被销毁
println!("The length of '{}' is {}.", s1, len); // s1 仍然有效
在这个例子中,&s1 创建了一个指向 s1 的引用。我们将这个引用传递给 calculate_length 函数。函数签名中的 s: &String 表明参数 s 是一个指向 String 类型的引用。
关键区别:
- 所有权转移(Move):如果我们将
s1直接传递给函数,s1的所有权会转移到函数参数中,之后s1将不再可用。 - 借用(Borrow):通过传递引用
&s1,我们只是“借出”了s1的使用权。函数结束后,借用结束,s1的所有者仍然是原来的变量,因此可以继续使用。
引用的基本规则
引用虽然灵活,但必须遵守 Rust 编译器强制执行的规则,以确保内存安全。这些规则是 Rust 借用检查器的核心:
- 在任何给定时间,你只能拥有一个可变引用,或者任意数量的不可变引用。
- 引用必须始终有效(不能有悬垂引用)。
不可变引用
默认情况下,通过 & 创建的引用是不可变引用。这意味着你不能通过这个引用修改它指向的值。这就像你借了一本书,但你不能在书上做任何标记或修改内容。
let s = String::from("hello");
let r1 = &s;
let r2 = &s; // 可以有多个不可变引用
println!("{}, {}", r1, r2); // 正确:只读访问
你可以同时拥有多个不可变引用,因为它们只是读取数据,不会造成数据竞争。
可变引用
如果你需要修改借用的数据,可以使用可变引用,通过 &mut 创建。这就像你借了一本可写的工作手册,可以在上面做笔记。
let mut s = String::from("hello"); // 变量本身必须是可变的
let r = &mut s; // 创建一个可变引用
r.push_str(", world"); // 通过可变引用修改数据
println!("{}", r); // 输出: hello, world
可变引用的限制:
- 同一作用域内,对于一个特定的数据,只能有一个可变引用。这是为了防止数据竞争(data race)。
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // 编译错误:不能同时拥有两个可变引用
println!("{}, {}", r1, r2);
- 你不能同时拥有一个可变引用和一个不可变引用。因为不可变引用假设数据不会改变,而可变引用可能会改变它。
let mut s = String::from("hello");
let r1 = &s; // 不可变引用
let r2 = &mut s; // 编译错误:不能同时拥有不可变引用和可变引用
println!("{}, {}", r1, r2);
为什么要有这些限制? 这些规则在编译时消除了数据竞争。数据竞争是一种非常难以追踪的 bug,通常发生在多线程或复杂的内存操作中。Rust 通过借用规则,在编译阶段就杜绝了这种可能性。
悬垂引用(Dangling References)
在 C 或 C++ 中,很容易创建指向已释放内存的指针,称为悬垂指针。Rust 编译器通过生命周期检查,确保引用永远不会变成悬垂引用。
例如,下面的代码会编译失败:
fn dangle() -> &String {
let s = String::from("hello");
&s // 返回 s 的引用
} // 这里 s 被销毁,其内存被释放。但引用仍然指向已释放的内存,这是危险的!
编译器会给出错误提示:“missing lifetime specifier”。因为函数返回的引用指向了函数内部创建的局部变量 s,而 s 在函数结束后就会被销毁。Rust 的借用检查器会阻止这种操作,确保引用始终指向有效的内存。
正确的做法是直接返回 String 本身,将所有权转移给调用者:
fn no_dangle() -> String {
let s = String::from("hello");
s // 返回 String,所有权转移
}
总结
借用和引用是 Rust 所有权系统的核心补充。它们允许你在不转移所有权的情况下安全地访问数据,同时通过编译器的严格检查,避免了悬垂指针和数据竞争等常见的内存安全问题。
- 引用:使用
&创建,允许你“借”用数据。 - 不可变引用(
&T):允许读取,不允许修改。可以有多个。 - 可变引用(
&mut T):允许读取和修改。同一时间只能有一个。 - 规则:引用必须始终有效,且不能同时存在可变引用和不可变引用(或两个可变引用)指向同一数据。
掌握这些规则,你将能更灵活地编写 Rust 代码,同时享受 Rust 带来的内存安全保障。
