第三章:引用与借用——不可变引用、可变引用、悬垂引用
所有权系统是Rust的基石,但说实话,如果每次传递参数都要转移所有权,那代码写起来也太痛苦了。你想想看,一个函数调用完,变量就没了,这谁受得了?
好在Rust提供了「引用」这个机制。它就像一把钥匙,让你能开门看看里面的东西,但不把房子搬走。我个人习惯把引用理解为「临时访问权限」,用完即还,所有权纹丝不动。
3.1 不可变引用:只读访问
不可变引用,说白了就是只读权限。你可以看,但不能改。
fn main() {
let s = String::from("hello");
let len = calculate_length(&s); // 传引用,不转移所有权
println!("'{}' 的长度是 {}", s, len); // s 还能用
}
fn calculate_length(s: &String) -> usize {
s.len()
} // 这里 s 离开作用域,但它不拥有所有权,所以不会 drop
注意看那个 &s,这就是创建引用的语法。函数签名里的 &String 表示「这是一个指向String的引用」。
我在项目中遇到过一个问题:新手容易把引用和所有权搞混,以为传了引用就能随便改。其实不行,编译器会直接报错——
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world"); // 编译错误!
}
错误信息很明确:cannot borrow `*some_string` as mutable, as it is behind a `&` reference。嗯,这里要注意,不可变引用就是只读的,想改?得用可变引用。
Copy trait,所以你可以创建任意多个不可变引用,它们之间互不干扰。
3.2 可变引用:读写权限
如果你真的需要修改借来的值,那就用可变引用。语法是 &mut。
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s); // 输出 "hello, world"
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
这里有两个关键点:
- 原变量必须声明为
mut - 函数参数类型必须是
&mut String
但可变引用有个严格的限制——同一作用域内,只能有一个可变引用。为什么?
我曾经在代码评审时见过这样的写法:
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // 编译错误!
println!("{}, {}", r1, r2);
编译器会告诉你:cannot borrow `s` as mutable more than once at a time。这个设计是为了防止数据竞争。你想想看,如果两个地方同时修改同一块内存,那结果谁说得准?
3.3 不可变引用与可变引用的共存规则
这里有个容易踩的坑:不可变引用和可变引用不能同时存在。但规则比你想的更精细——
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 编译错误!
println!("{}, {}, and {}", r1, r2, r3);
为什么?因为不可变引用假设「没人会改数据」,而可变引用说「我要改数据」。这两个假设是矛盾的。Rust编译器不允许这种矛盾存在。
但如果你不再使用不可变引用,就可以创建可变引用:
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2); // r1, r2 最后一次使用
// 这里 r1, r2 的作用域结束
let r3 = &mut s; // 没问题!
println!("{}", r3);
这个特性叫「非词法作用域生命周期」(NLL),Rust 2018引入的。说白了就是:编译器会追踪引用最后一次使用的位置,在那之后就可以创建新的可变引用了。
- 要么有任意多个不可变引用(&T)
- 要么有且只有一个可变引用(&mut T)
- 两者不能同时存在
3.4 悬垂引用:Rust如何消灭野指针
悬垂引用,就是指向已经释放的内存的指针。在C/C++里这叫「野指针」,是段错误的常见元凶。Rust在编译期就帮你消灭了这个问题。
看这个危险的例子:
fn dangle() -> &String {
let s = String::from("hello");
&s // 返回指向 s 的引用
} // 这里 s 被 drop,引用变成悬垂指针
编译器会直接拒绝:missing lifetime specifier。Rust说:「你返回的引用指向一个即将被释放的值,这不行。」
正确的做法是把所有权转移出去:
fn no_dangle() -> String {
let s = String::from("hello");
s // 转移所有权,没问题
}
我记得刚学Rust时,总想返回引用省点拷贝开销。后来发现,Rust的编译器比我想得周到——它强制我思考「这个引用到底指向谁?它的生命周期够不够长?」
Rc<T> 或 Arc<T> 共享所有权。
3.5 引用的规则总结
把引用规则整理成一张表,方便你随时查阅:
| 规则 | 说明 | 违反后果 |
|---|---|---|
| 同一时间只能有一个可变引用 | 防止数据竞争 | 编译错误 |
| 不可变引用和可变引用不能共存 | 保证读取时数据不变 | 编译错误 |
| 引用必须始终有效 | 防止悬垂指针 | 编译错误 |
| 引用不拥有所有权 | 离开作用域不会drop数据 | 无(这是特性) |
3.6 知识体系图
下面这张图展示了引用与借用的核心逻辑,我建议你多看几遍,理解其中的关系:
这张图把三种引用类型放在一起对比。你看,不可变引用和可变引用是「合法」的,但规则不同;悬垂引用则是Rust坚决禁止的。理解这三者的区别,你就掌握了Rust内存安全的核心。
嗯,引用与借用的内容就到这里。记住:Rust的引用规则不是限制你,而是保护你。等你习惯了这套规则,你会发现自己写的代码越来越健壮,越来越不容易出bug。
公众号:蓝海资料掘金营,微信deep3321