序
在上文 一文通关所有权(Ownership)系统 中,我们知晓了 引用 是 不获取所有权而访问数据的重要方式。
不获取所有权而访问数据 这一行为就是 借用。
借用的基础概念
回顾之前的示例
fn main() {
let mut s1 = String::from("hello");
let s2 = &mut s1;
s2.push_str(", world");
println!("s2: {}", s2); // s2: hello, world
println!("s1: {}", s1); // s1: hello, world
}
s1 是数据的所有者。s2 借用了 s1 的数据,也可以说 s1 被 s2 借用。
借用分类
不可变借用 (&T)
可以通过引用读取数据,但不能修改它
fn main() {
let s = String::from("hello");
let r1 = &s; // 不可变借用
r1.push('a'); // error: cannot borrow `s` as mutable, as it is not declared as mutable
println!("{}", r1);
}
- 通过 不可变引用(&) 来借用数据
s被r1不可变借用 (&T)- 不可通过引用
r1修改数据
可变借用 (&mut T)
可以通过引用读取数据,也可以修改它
fn main() {
let mut s = String::from("hello");
let r1 = &mut s; // 不可变借用
r1.push('a'); // 可变借用
println!("{}", r1); // helloa
}
- 通过 可变引用(&mut) 来借用数据
s被r1可变借用 (&mut T)- 可以通过引用
r1修改数据
借用规则
Rust 的借用规则是为了确保内存安全并防止数据竞争。主要有以下三条规则:
- 在同一作用域内,可以有任意多个不可变引用,或者只能有一个可变引用。
- 可变引用与不可变引用不能同时存在。
- 引用必须总是有效,也就是说,不能在引用有效期间使其所指向的数据失效。
多个不可变引用
fn main() {
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("r1: {}, r2: {}", r1, r2); // 两个不可变引用可以共存
}
单一可变引用
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s; // 错误!同时只能有一个可变引用
r1.push_str(", world");
println!("{}", r1);
}
不可变引用与可变引用的冲突
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s; // r1的借用作用域内, 再使用不可变引用, 会发生冲突
println!("r1: {}", r1);
}
引用必须总是有效
fn main() {
let x = 5;
let x2 = x_self(x);
println!("The value of x2 is: {}", x2);
}
fn x_self (x: i32) -> &i32 { // 错误:返回的引用是悬垂引用
println!("The value of x is: {}", x);
return &x;
}
x_self 函数返回了一个引用,但是引用的数据 x 在函数结束后就会被销毁。因此返回的引用是 悬垂引用 (已经释放或无效内存地址的引用)。
这种行为是不允许的,Rust 编译器会报错。
借用作用域
借用的作用域是指借用的有效范围。
在这个范围内,你不能对原值进行修改,除非借用已经结束。
let mut arr = [1, 2, 3, 4, 5];
let slice = &mut arr[1..3]; // 可变借用 arr 的一部分
// 此时 arr 的部分数据被借用,不能修改其他部分
// arr[0] = 10; // 错误:arr 仍被部分借用
slice[0] = 20; // 正确,修改切片内容
println!("{:?}", slice); //
// 借用结束后,可以修改 arr
arr[0] = 10;
println!("{:?}", arr); // 输出 [10, 20, 3, 4, 5]
可变借用在使用结束后立即释放,作用域结束后原数据便可重新使用。
生命周期(Lifetimes)
值的生命周期
一个值在内存中存在的时间段,也就是它被创建到被销毁的时间范围。
当值的所有者(比如在栈上的局部变量)离开作用域时,值的生命周期就结束了,Rust 会自动释放值所占的内存。
值的生命周期通常和它的作用域一致,而作用域结束时值会被自动丢弃。
引用的生命周期
引用在作用域中有效的时间段。
当你借用一个值时,就创建了一个引用的生命周期。当引用不再被使用时,引用的生命周期就会结束,即使它的值仍然存在。
引用的生命周期必须与它所借用的数据的生命周期匹配。
生命周期示例
fn main() {
let r;
{
let x = 5;
r = &x; // 错误:x 的生命周期结束,r 引用了无效数据
}
// println!("r: {}", r); // 错误,x 已经不再存在
}
这也是我们之前示例中
fn x_self (x: i32) -> &i32 { // 错误:返回的引用是悬垂引用
println!("The value of x is: {}", x);
return &x;
}
这段函数不会通过编译的原因。因为 x 的生命周期已经结束了,而引用的生命周期必须与它所借用的数据的生命周期匹配。
生命周期标注
Rust 的生命周期机制确保引用永远不会无效。
每个引用都有一个生命周期,它标记了引用的有效范围。
当函数返回引用时,Rust 需要知道引用的生命周期,以确保返回的引用不会在调用者作用域之外失效。
// 'a 是生命周期标注,表示返回的引用的生命周期与传入的引用相同
// Rust 对生命周期标注使用的名称没有要求,只要以 ' 开头。因此,'a、'b、'c 等都是可以的。
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
很多情况下,Rust 不需要你手动添加生命周期标注,编译器会自动处理。但在复杂引用关系中,编译器无法自行推断时,就需要显式标注。
这里 s1 和 s2 都是引用,如果不显示说明,Rust 便无法推断返回值使用谁的生命周期。
其他: 泛型语法
<> 表示泛型参数, 与 TypeScript 中的泛型类似。
可以组合多种参数,例如使用多个生命周期标注和多个泛型参数:
fn complex<'a, 'b, T, U>(s1: &'a str, s2: &'b str, value: T, other: U) {
// ...
}