一、什么是引用?怎么借?
看代码:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // &s1 创建了一个指向 s1 的引用,但所有权没变
println!("'{}' 的长度是 {}", s1, len); // s1 依然有效
}
fn calculate_length(s: &String) -> usize { // s 是一个对 String 的引用
s.len()
} // s 离开作用域,但它只是引用,不会释放所指向的数据我们把 &s1 这个动作叫做借用,s 就是一个引用。你可以把引用想象成一本书的目录条目——它能让你看到书的内容,但条目本身并不是书。把书借给别人看,书还是你的。
引用默认是不可变的,你不能通过它去修改原值:
fn change(s: &String) {
s.push_str(", world"); // 编译错误!不能通过不可变引用修改
}如果确实需要修改,就得用可变引用。
二、可变引用:借走还能改
fn main() {
let mut s = String::from("hello");
change(&mut s); // 创建可变引用
println!("{}", s); // 输出 "hello, world"
}
fn change(s: &mut String) {
s.push_str(", world");
}可变引用的规则非常严格,这正是 Rust 把数据竞争扼杀在编译期的关键。
规则一:同一作用域,对同一数据,只能有一个可变引用
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // 编译错误!不能同时拥有两个可变引用
println!("{}, {}", r1, r2);如果允许两个可变引用同时存在,就可能出现“你改我也改”的数据竞争。Rust 直接让这种代码编译不过。
注意:这个限制是“同一作用域”内,且引用必须没有失效。 如果第一个可变引用在第二个被创建前已经用完了,编译器是允许的:
let mut s = String::from("hello");
let r1 = &mut s;
println!("{}", r1); // r1 最后一次使用在此,之后 r1 不再被使用
let r2 = &mut s; // 编译器允许,因为 r1 已经“失效”
println!("{}", r2);编译器会进行一种非词法作用域生命周期分析,通俗说就是:只要你后面不再用 r1,它就算提前“死”了,就可以再借给别人。
规则二:可变引用和不可变引用不能同时存在
let mut s = String::from("hello");
let r1 = &s; // 不可变引用
let r2 = &s; // 没问题,多个不可变引用可以共存
let r3 = &mut s; // 编译错误!已经存在不可变引用,不能再有可变引用
println!("{}, {}, {}", r1, r2, r3);反过来也一样,如果已经有了一个可变引用,就不能再有任何不可变引用。
为什么? 假设你有了一个不可变引用,你预期它指向的值不会变。如果这时再冒出一个可变引用并修改了数据,你的不可变引用就读到被污染的数据了。Rust 通过这个规则在编译期杜绝了“读写冲突”。
同样,如果不可变引用在可变引用创建前就已经不再使用,编译器也会放行:
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2); // r1 和 r2 此后不再使用
let r3 = &mut s; // 允许
println!("{}", r3);三、数据竞争:Rust 帮你关在门外的恶魔
数据竞争是一种非常难复现和调试的 bug,它发生在三个条件同时满足时:
- 两个或以上指针同时访问同一数据。
- 至少有一个指针用于写入。
- 没有同步机制。
Rust 通过所有权和借用规则,让编译器在编译期就能证明你的代码没有数据竞争:
- 同一时间只能有一个可变引用(解决了“同时有两个写入指针”的问题)。
- 可变引用与不可变引用不能共存(解决了“一个写一个读”的问题)。
因此,在安全的 Rust 代码中,你永远不需要担心数据竞争。
四、悬垂引用:指向已释放内存的“鬼魂”
在 C/C++ 里,你可能写出这样的代码:函数返回一个局部变量的指针,等函数结束后指针就成了野指针。Rust 编译器保证你永远写不出悬垂引用。
fn dangle() -> &String { // 期望返回一个 String 引用
let s = String::from("hello"); // s 在堆上分配
&s // 返回 s 的引用
} // s 离开作用域,内存被释放!返回的引用指向已释放的内存编译器会果断报错:
error[E0106]: missing lifetime specifier
...
this function's return type contains a borrowed value, but there is no value for it to be borrowed from它直接指出:这个借用的值不存在于函数之外,你不能返回一个对局部变量的引用。
解决办法很简单:直接把所有权交出去,返回 String 而不是 &String。
fn no_dangle() -> String {
let s = String::from("hello");
s // 所有权移出,调用者获得所有权
}五、引用的本质与生命周期初探
每一个引用背后都有一个生命周期,它是编译器用来跟踪引用作用域的工具。上面我们讲的“最后一次使用”其实就是编译器自动推断引用生命周期的结果。
简单的例子:
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}这段代码会编译失败,因为编译器不知道返回的引用是来自 x 还是 y,也就无法确定返回的引用能活多久。你需要显式标注生命周期参数,告诉编译器输入引用和输出引用之间的生存时间关系。
总结(帮你串联)
- 引用让你在不获取所有权的情况下使用数据,分不可变引用 (
&T) 和可变引用 (&mut T)。 - 借用规则(编译期强制检查):
- 同一时刻,要么任意多个不可变引用,要么恰好一个可变引用,两者不能共存。
- 引用必须永远有效(不存在悬垂引用)。
- 这些规则消灭了数据竞争和悬垂指针。
- 编译器会进行非词法作用域分析,只要引用不再使用,就会提前结束借用,允许新的借用。
理解了这些,你就已经握住了 Rust 内存安全的核心钥匙。所有权决定了谁来释放内存,借用决定了谁能访问内存以及怎么访问。两者结合,就是一套零成本、完全编译期检查的内存管理方案。