引用与借用
借用的动机
在上一章我们看到,将数据传入函数会转移所有权,函数返回后原变量失效。这非常不便——难道我们每次都要把数据传进函数,再返回回来吗?
// 不使用借用:必须返回所有权
fn calculate_length(s: String) -> (String, usize) {
let len = s.len();
(s, len) // 必须把 s 还回去!
}
fn main() {
let s1 = String::from("hello");
let (s1, len) = calculate_length(s1); // 传进去再拿回来,很笨拙
println!("'{}' 的长度是 {}", s1, len);
}
Rust 的解决方案是借用(borrowing):使用 & 创建对值的引用,引用可以"借用"数据而不取得所有权。
// 使用借用:参数是引用,不转移所有权
fn calculate_length(s: &String) -> usize {
s.len()
} // s 是借用,离开作用域时不会 drop 它指向的 String
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // 借用 s1,不转移所有权
println!("'{}' 的长度是 {}", s1, len); // s1 仍然有效
}
借用规则
两条核心借用规则
规则一:在任意时刻,你可以拥有任意数量的不可变引用(&T),或者恰好一个可变引用(&mut T),但两者不能同时存在。
规则二:引用必须始终有效——引用不能比它所指向的数据活得更长。
这两条规则从根本上防止了所有的数据竞争和悬垂引用问题。让我们逐一理解:
不可变借用:可以有多个
fn main() {
let s = String::from("hello");
// 多个不可变借用同时存在:合法!
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("{} {} {}", r1, r2, r3); // 正常
// 多个读者同时读取同一数据:安全,不会产生竞争
}
可变借用:同一时刻只能有一个
fn main() {
let mut s = String::from("hello");
// 一个可变借用
let r1 = &mut s;
r1.push_str(" world");
println!("{}", r1);
// 编译错误!不能同时有两个可变借用
// let r2 = &mut s; // error: cannot borrow `s` as mutable more than once
// 编译错误!不可变借用和可变借用不能同时存在
// let r2 = &s; // error: cannot borrow `s` as immutable because it is also borrowed as mutable
}
为什么这样设计?防止数据竞争
数据竞争(Data Race)是并发编程中最难追踪的 bug 类型,发生条件是:
- 两个或更多指针同时访问同一块数据
- 至少有一个指针在写入数据
- 没有同步机制
Rust 的借用规则在编译期就防止了这三个条件同时满足:任意时刻,要么多个读者(不可变引用),要么恰好一个写者(可变引用),永远不会同时存在读者和写者。
从 Rust 2018 Edition 开始,Rust 引入了非词法生命周期(Non-Lexical Lifetimes, NLL):引用的生命周期结束于它最后一次使用的地方,而不是词法上的作用域结束处。这让借用检查器更智能,减少了不必要的编译错误。
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 不可变借用开始
let r2 = &s; // 另一个不可变借用开始
println!("{} {}", r1, r2);
// ↑ NLL:r1 和 r2 在这里最后使用,它们的生命周期在此结束
// 所以这里可以安全地创建可变借用!
let r3 = &mut s;
r3.push_str(" world");
println!("{}", r3);
}
悬垂引用:Rust 编译器防止的 bug
什么是悬垂引用?
悬垂引用(Dangling Reference)是指引用指向的内存已经被释放,但引用本身仍然存在。这是 C/C++ 中极其危险的 bug——读取悬垂指针会得到垃圾数据或导致崩溃,而且很难调试。
// Rust 不允许你写出这样的代码
fn dangle() -> &String { // 试图返回对局部变量的引用
let s = String::from("hello");
&s
// ↑ 编译错误!s 在函数结束时被 drop,
// 返回的引用将指向已释放的内存——悬垂引用!
// error[E0106]: missing lifetime specifier
}
// 正确做法:直接返回 String(转移所有权)
fn no_dangle() -> String {
let s = String::from("hello");
s // 所有权移出,不会被 drop
}
生命周期(Lifetimes)
生命周期是什么?
生命周期是 Rust 类型系统的一部分,用来追踪引用的有效时间范围。大多数情况下生命周期是隐式推断的,但有时编译器无法独立推断,需要你明确标注。
生命周期的作用是:让编译器验证"没有任何引用的有效期会超过它指向的数据的有效期"。
生命周期标注语法
// 生命周期参数:用 'a、'b 等表示(以单引号开头)
// 这个函数返回两个字符串中较长的那个
// 编译器无法单独推断:返回值的生命周期取决于哪个参数更长
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
// 生命周期 'a 表示:返回值的有效期不超过 x 和 y 中较短的那个
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("long string");
let result;
{
let s2 = String::from("xy");
result = longest(s1.as_str(), s2.as_str());
println!("较长的是: {}", result);
}
// 如果在这里使用 result,编译器会报错
// 因为 s2 在内部块结束时被 drop,result 可能指向它
}
生命周期标注不改变运行行为
重要:生命周期标注不会改变引用的实际有效时间,它只是告诉编译器多个引用之间的关系,让编译器能够验证你的代码是否安全。生命周期是"给编译器的注释"。
结构体中的生命周期
// 结构体包含引用时,必须标注生命周期
struct Important<'a> {
part: &'a str, // 这个引用的有效期至少和结构体实例一样长
}
impl<'a> Important<'a> {
fn announce(&self, announcement: &str) -> &str {
println!("注意!{}", announcement);
self.part
}
}
fn main() {
let novel = String::from("这是一个故事。很久很久以前...");
let first_sentence: &str;
{
let i = Important {
part: novel.split('。').next().expect("找不到句号"),
};
first_sentence = i.announce("重要通知");
println!("{}", first_sentence);
}
}
生命周期省略规则(Elision Rules)
在很多常见模式中,Rust 编译器能自动推断生命周期,你不需要显式标注。这被称为"生命周期省略规则":
fn foo(x: &i32, y: &i32) → fn foo<'a,'b>(x: &'a i32, y: &'b i32)fn foo(x: &i32) -> &i32 → fn foo<'a>(x: &'a i32) -> &'a i32&self 或 &mut self,self 的生命周期被赋予所有输出引用。这是最常见的情况。'static 生命周期
// 'static 表示整个程序生命周期内有效
// 字符串字面量自动拥有 'static 生命周期
let s: &'static str = "我永远有效";
// 函数签名中的 'static 约束
fn need_static(s: &'static str) {
println!({}, s);
}
看到编译错误时,不要无脑地给所有生命周期标注 'static——这通常是逃避问题,而不是解决问题。'static 意味着引用必须整个程序生命周期内有效,这在很多场景下过于严格。真正的解决方案是理清数据的所有权关系。