今日主题:References and Borrowing
第一层:原理层(Why & How)
为什么需要借用?
Rust 的所有权系统。所有权规则很严格:
- 每个值有且只有一个所有者
- 当所有者离开作用域,值被丢弃
这带来一个问题:如果我们想多次使用一个值,但又不想转移所有权怎么办?
// 问题:s 被 move 到 calculate_length,之后无法再使用
let s = String::from("hello");
let len = calculate_length(s); // s 被 move
println!("{}", s); // ❌ 编译错误!s 已失效
解决方案:借用(Borrowing)——允许你使用值,但不获取所有权。
引用的本质
引用(Reference)是一个指向值的指针,但不拥有该值。
栈内存 堆内存
┌─────────────┐ ┌─────────────┐
│ s │─────────►│ "hello" │
│ (String) │ │ │
└─────────────┘ └─────────────┘
│
│ 引用 &s
▼
┌─────────────┐
│ &s │──────┐
│ (&String) │ │
└─────────────┘ └─────► 只读访问,不拥有所有权
借用规则(Borrowing Rules)
Rust 编译器在编译时强制执行以下规则:
- 任何时刻,要么有一个可变引用,要么有任意数量的不可变引用
- 引用必须总是有效的(不能指向已释放的内存)
借用规则示意图:
同一时间:
✅ &T + &T + &T 多个不可变引用
✅ &mut T 一个可变引用
❌ &mut T + &mut T 多个可变引用(数据竞争)
❌ &mut T + &T 可变 + 不可变(读写冲突)
为什么这些规则能保证安全?
数据竞争(Data Race) 的条件:
- 两个或多个指针同时访问同一数据
- 至少有一个是写操作
- 没有同步机制
Rust 的借用规则在编译期就阻止了数据竞争的可能性:
- 如果有
&mut T,保证没有其他引用存在 → 独占访问,安全 - 如果只有
&T,保证没有可变引用 → 只读访问,安全
生命周期与引用有效性
Rust 通过**生命周期(Lifetime)**确保引用总是指向有效数据:
{
let r; // r 的生命周期开始
{
let x = 5; // x 的生命周期开始
r = &x; // r 借用 x
} // x 被释放,r 变成悬垂引用
println!("{}", r); // ❌ 编译错误!r 指向无效内存
} // r 的生命周期结束
编译器会检查引用的生命周期不超过被引用值的生命周期。
第二层:实战层(What & Do)
不可变引用
fn main() {
let s = String::from("hello");
// 创建不可变引用
let len = calculate_length(&s);
// s 仍然有效,因为只是借用,不是 move
println!("字符串 '{}' 的长度是 {}", s, len);
}
// 参数类型 &String 表示接受 String 的引用
fn calculate_length(s: &String) -> usize {
s.len() // 通过引用访问值,不需要解引用
}
关键点:
&s创建引用&String是引用类型- 使用引用时不需要
*s,Rust 自动解引用
可变引用
fn main() {
let mut s = String::from("hello");
// 创建可变引用
change(&mut s);
println!("{}", s); // 输出: hello, world
}
// &mut String 表示可变引用
fn change(s: &mut String) {
s.push_str(", world"); // 可以修改引用的值
}
关键点:
&mut s创建可变引用- 原变量必须是
mut - 可变引用允许修改值
多重不可变引用
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;
// let r2 = &mut s; // ❌ 错误!不能有两个可变引用
println!("{}", r1);
}
混合引用的限制
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 不可变引用
let r2 = &s; // 另一个不可变引用
// let r3 = &mut s; // ❌ 错误!不能同时有不可变和可变引用
println!("{} and {}", r1, r2);
// r1 和 r2 不再使用,可以创建可变引用了
let r3 = &mut s; // ✅ 可以了
r3.push_str("!");
println!("{}", r3);
}
悬垂引用(Dangling References)
// ❌ 错误示例
fn dangle() -> &String { // 返回引用
let s = String::from("hello");
&s // 返回 s 的引用
} // s 在这里离开作用域,内存被释放
// 返回的引用指向无效内存!
// ✅ 正确做法:返回所有权
fn no_dangle() -> String {
let s = String::from("hello");
s // 返回 String,所有权转移
}
引用的作用域
fn main() {
let mut s = String::from("hello");
{
let r1 = &mut s; // r1 的作用域开始
r1.push_str(" world");
} // r1 在这里结束
// r1 已经结束,可以创建新的引用
let r2 = &mut s;
println!("{}", r2);
}
切片(Slice)——特殊的引用
切片是对集合的部分引用:
fn main() {
let s = String::from("hello world");
// 字符串切片
let hello = &s[0..5]; // 从索引 0 到 4
let world = &s[6..11]; // 从索引 6 到 10
println!("{} {}", hello, world);
// 数组切片
let arr = [1, 2, 3, 4, 5];
let slice = &arr[1..3]; // [2, 3]
println!("{:?}", slice);
}
// 接受字符串切片(更通用)
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[..]
}
为什么 &str 比 &String 更好?
&str可以接收String和字符串字面量- 更灵活,更通用
第三层:最佳实践(Production Ready)
1. 优先使用不可变引用
// ✅ 好:只在需要修改时使用可变引用
fn process_data(data: &Vec<i32>) -> i32 {
data.iter().sum()
}
// ❌ 坏:不必要的可变引用
fn process_data_bad(data: &mut Vec<i32>) -> i32 {
data.iter().sum()
}
2. 使用切片类型增加灵活性
// ✅ 好:接受字符串切片
fn parse_config(content: &str) -> Config {
// 可以接收 &String 和 &str
}
// ❌ 坏:只接受 String 引用
fn parse_config_bad(content: &String) -> Config {
// 不能接收字符串字面量
}
3. 避免过长的借用
// ❌ 坏:借用时间过长
let data = get_data();
let ref1 = &data;
let ref2 = &data;
// ... 很多代码 ...
// ref1 和 ref2 一直存活,阻止了可变借用
// ✅ 好:限制借用范围
let result = {
let data = get_data();
let refs = (&data, &data);
process(refs)
}; // 借用在这里结束
// 现在可以修改 data 了
4. 使用结构体组织相关引用
// ✅ 好:用结构体封装相关数据
struct ParsedData<'a> {
header: &'a str,
body: &'a str,
footer: &'a str,
}
fn parse_document(content: &str) -> ParsedData {
ParsedData {
header: &content[0..10],
body: &content[10..100],
footer: &content[100..],
}
}
5. 避免返回局部变量的引用
// ❌ 坏:返回局部变量的引用
fn get_string() -> &String {
let s = String::from("hello");
&s // s 会被释放,返回悬垂引用
}
// ✅ 好:返回所有权
fn get_string() -> String {
let s = String::from("hello");
s
}
// ✅ 好:返回静态生命周期的引用
fn get_static_str() -> &'static str {
"hello" // 字符串字面量是 'static
}
6. 使用 Cow(Clone on Write)优化
use std::borrow::Cow;
// ✅ 好:避免不必要的克隆
fn process_string(s: Cow<str>) -> String {
if s.contains("special") {
// 需要修改时才克隆
s.into_owned().replace("special", "normal")
} else {
// 不需要修改,不克隆
s.into_owned()
}
}
// 使用
let owned = String::from("hello");
process_string(Cow::Owned(owned));
let borrowed = "hello";
process_string(Cow::Borrowed(borrowed));
7. 文档注释说明借用关系
/// 解析配置文件
///
/// # Arguments
/// * `content` - 配置文件内容,函数执行期间必须保持有效
///
/// # Returns
/// 返回配置结构体,不包含对 `content` 的引用
pub fn parse_config(content: &str) -> Config {
// ...
}
第四层:问题诊断(Troubleshooting)
问题 1:cannot borrow as mutable more than once
错误信息:
error[E0499]: cannot borrow `s` as mutable more than once at a time
代码示例:
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // ❌ 错误!
解决:
let mut s = String::from("hello");
{
let r1 = &mut s;
// 使用 r1
}
let r2 = &mut s; // ✅ r1 已经结束,可以创建 r2
问题 2:cannot borrow as mutable because it is also borrowed as immutable
错误信息:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
代码示例:
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s; // ❌ 错误!
println!("{}", r1);
解决:
let mut s = String::from("hello");
let r1 = &s;
println!("{}", r1); // 先使用完不可变引用
let r2 = &mut s; // ✅ 现在可以创建可变引用了
r2.push_str(" world");
问题 3:does not live long enough(悬垂引用)
错误信息:
error[E0597]: `s` does not live long enough
代码示例:
fn get_ref() -> &String {
let s = String::from("hello");
&s // ❌ s 会被释放
}
解决:
// ✅ 返回所有权
fn get_string() -> String {
let s = String::from("hello");
s // 所有权转移
}
// ✅ 或者返回 'static 引用
fn get_static_str() -> &'static str {
"hello"
}
问题 4:slice indices are out of order
错误信息:
error: slice indices are out of order
代码示例:
let s = String::from("hello");
let slice = &s[3..1]; // ❌ 起始 > 结束
解决:
let slice = &s[1..3]; // ✅ 起始 <= 结束
问题 5:mismatched types(生命周期不匹配)
错误信息:
error[E0623]: lifetime mismatch
代码示例:
fn longest(x: &str, y: &str) -> &str { // ❌ 缺少生命周期标注
if x.len() > y.len() { x } else { y }
}
解决:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
第五层:权威引用(References)
- References and Borrowing - Rust Book - Rust 官方教程
- The Rust Programming Language - Ownership - 所有权系统详解
- Rust Reference - References - 引用类型规范
- The Rustonomicon - Ownership - 高级所有权概念
- Rust By Example - Scoping rules - 作用域示例
今日总结
- 借用允许使用值而不获取所有权,通过
&T(不可变)和&mut T(可变)创建 - 借用规则:同时只能有一个可变引用或任意数量的不可变引用
- 引用必须有效,编译器通过生命周期检查确保不返回悬垂引用
- 切片
&str和&[T]是对集合的部分引用,比完整引用更灵活 - 最佳实践:优先使用不可变引用、使用切片类型、限制借用范围、避免返回局部引用