在 Rust 中,若仅通过转移所有权获取值,会使程序复杂度增加。而"引用与借用"机制能像其他编程语言的指针或引用一样,让我们在不转移所有权的前提下使用变量,极大地提升了代码的灵活性与简洁性。本教程将详细讲解引用与借用的核心概念、用法及规则。
一、引用与解引用
1. 引用的定义
常规引用是一种指针类型,指向对象存储的内存地址。通过引用,我们可以访问变量的值,但不会获取变量的所有权。
2. 基础示例
rust
fn main() {
let x = 5;
let y = &x; // 创建x的引用y
assert_eq!(5, x); // 直接访问x的值,断言成功
assert_eq!(5, *y); // 使用解引用运算符*获取y指向的值,断言成功
}
&x:表示创建变量x的引用,y的类型为&i32。*y:解引用运算符,用于获取引用y所指向的实际值。
3. 常见错误:未解引用的比较
若直接比较整数与引用,会因类型不匹配导致编译错误:
rust
// 错误代码
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, y); // 错误:无法比较`i32`类型与`&i32`类型
}
编译错误信息:
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src/main.rs:4:5
|
4 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
|
= help: the trait `std::cmp::PartialEq<&{integer}>` is not implemented for `{integer}`
二、不可变引用
1. 概念与作用
不可变引用允许我们访问变量的值,但不允许修改该值,且不会获取变量的所有权。当引用离开作用域时,其指向的值不会被丢弃。
2. 函数传参示例
通过不可变引用向函数传递参数,避免所有权转移:
rust
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // 传递s1的不可变引用
// 由于未转移所有权,s1仍可正常使用
println!("The length of '{}' is {}.", s1, len);
}
// 函数参数为&String类型(String的不可变引用)
fn calculate_length(s: &String) -> usize {
s.len() // 访问引用指向的值的长度,无需解引用(Rust自动解引用优化)
} // s离开作用域,因不拥有所有权,无任何资源释放操作
3. 错误尝试:修改不可变引用
若尝试通过不可变引用修改值,会触发编译错误:
rust
// 错误代码
fn main() {
let s = String::from("hello");
change(&s); // 传递不可变引用
}
fn change(some_string: &String) {
some_string.push_str(", world"); // 尝试修改不可变引用指向的值,错误
}
编译错误信息:
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
|
7 | fn change(some_string: &String) {
| ------- help: consider changing this to be a mutable reference: `&mut String`
8 | some_string.push_str(", world");
| ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
三、可变引用
1. 概念与用法
可变引用允许我们修改其指向的值,但需满足特定规则。使用可变引用需同时满足两个条件:
- 变量声明为
mut(可变)。 - 引用通过
&mut创建,函数参数类型需匹配为&mut T。
2. 正确示例
rust
fn main() {
let mut s = String::from("hello"); // 变量s声明为可变
change(&mut s); // 传递s的可变引用
println!("{}", s); // 输出:hello, world
}
// 函数参数为&mut String(String的可变引用)
fn change(some_string: &mut String) {
some_string.push_str(", world"); // 成功修改引用指向的值
}
3. 核心限制1:同一作用域仅一个可变引用
同一作用域内,特定数据只能有一个可变引用,此规则用于避免数据竞争:
rust
// 错误代码
fn main() {
let mut s = String::from("hello");
let r1 = &mut s; // 第一个可变引用
let r2 = &mut s; // 第二个可变引用,错误
println!("{}, {}", r1, r2);
}
编译错误信息:
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:5:14
|
4 | let r1 = &mut s;
| ------ first mutable borrow occurs here
5 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
7 | println!("{}, {}", r1, r2);
| -- first borrow later used here
4. 解决方案:手动限制作用域
通过大括号{}手动划分作用域,可创建多个可变引用(非同一作用域):
rust
fn main() {
let mut s = String::from("hello");
{
let r1 = &mut s; // r1的作用域仅限于此大括号内
println!("{}", r1); // 输出:hello
} // r1离开作用域,可变引用释放
let r2 = &mut s; // 可创建新的可变引用r2
println!("{}", r2); // 输出:hello
}
5. 核心限制2:可变与不可变引用不能同时存在
若数据已存在不可变引用,同一作用域内不能再创建可变引用(避免不可变引用读取到被修改后的数据):
rust
// 错误代码
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 不可变引用
let r2 = &s; // 另一个不可变引用(允许)
let r3 = &mut s; // 可变引用,错误
println!("{}, {}, and {}", r1, r2, r3);
}
编译错误信息:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:6:14
|
4 | let r1 = &s; // 没问题
| -- immutable borrow occurs here
5 | let r2 = &s; // 没问题
6 | let r3 = &mut s; // 大问题
| ^^^^^^ mutable borrow occurs here
8 | println!("{}, {}, and {}", r1, r2, r3);
| -- immutable borrow later used here
6. 编译器优化:引用作用域缩短(NLL)
Rust 引入 Non-Lexical Lifetimes(NLL,非词法生命周期) 优化,使引用的作用域从"花括号结束"缩短为"最后一次使用的位置",提升代码灵活性:
rust
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2); // r1、r2最后一次使用,作用域结束
let r3 = &mut s; // 允许创建可变引用,无冲突
println!("{}", r3); // 输出:hello
}
- 老版本编译器(Rust 1.31 前):
r1、r2作用域到函数结束,此代码会报错。 - 新版本编译器:
r1、r2作用域在println!后结束,代码正常编译。
四、悬垂引用(Dangling References)
1. 概念
悬垂引用指指针指向的值被释放后,指针仍存在,可能指向无效内存或被其他变量复用,会导致未定义行为。Rust 编译器通过生命周期检查,确保引用永远不会悬垂。
2. 错误示例:创建悬垂引用
rust
// 错误代码
fn main() {
let reference_to_nothing = dangle(); // 接收悬垂引用
}
// 尝试返回字符串的引用,会导致悬垂
fn dangle() -> &String {
let s = String::from("hello"); // s在函数内创建
&s // 返回s的引用
} // s离开作用域,被释放,引用指向无效内存
编译错误信息:
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
|
5 | fn dangle() -> &'static String {
| ~~~~~~~~
关键错误原因:函数返回借用值,但找不到其借用的原数据(原数据已被释放)。
3. 解决方案:返回所有权而非引用
直接返回值的所有权,避免返回引用:
rust
fn main() {
let string = no_dangle(); // 接收String的所有权
println!("{}", string); // 输出:hello
}
// 返回String而非引用,所有权转移给调用者
fn no_dangle() -> String {
let s = String::from("hello");
s // 返回s,转移所有权
}
五、借用规则总结
- 引用有效性:引用必须始终有效,不能指向已释放的数据(无悬垂引用)。
- 可变引用限制:同一时刻,特定数据只能有一个可变引用,或任意多个不可变引用(二选一)。
- 作用域优化:通过 NLL 优化,引用作用域缩短为最后一次使用的位置,提升代码灵活性。