在 Rust 中,所有权系统是内存安全的基石,但严格的所有权转移规则(值的所有权一旦转移,原变量即失效)在实际开发中会带来不便------如果我们只是想临时使用某个值,而非永久获取其所有权,频繁的转移与复制会让代码变得繁琐且低效。
为解决这一问题,Rust 设计了**借用(Borrowing)**机制:允许我们通过"引用(Reference)"临时访问某个值,而不获取其所有权。借用既保留了所有权系统的内存安全保障,又大幅提升了代码的灵活性。本文将从基础概念到进阶技巧,全面拆解 Rust 借用的核心逻辑、使用规则与实战场景,搭配详细示例代码帮你彻底掌握这一核心特性。
一、前置回顾:为什么需要借用?
在讲解借用之前,我们先通过一个所有权转移的示例,感受一下"无借用"时的痛点:
rust
fn main() {
let s = String::from("hello");
// 所有权从 s 转移到 print_string 函数的参数
print_string(s);
// 错误!s 的所有权已转移,无法再使用
// println!("{}", s);
}
// 接收 String 类型,获取所有权
fn print_string(s: String) {
println!("{}", s);
} // s 离开作用域,堆内存被释放
上述代码中,print_string 函数仅需"打印字符串"这一临时操作,却不得不获取 s 的所有权,导致 main 函数中 s 后续无法使用。如果想让 s 继续可用,只能在函数内复制 s 并返回,这会增加不必要的性能开销(尤其是堆上大数据)。
而借用机制正是为解决这种"临时访问"需求而生:通过引用传递参数,函数仅临时"借用"值的访问权,不获取所有权,函数执行完毕后,引用失效,原变量的所有权仍保留在调用者手中。
二、借用的基础:引用的定义与分类
借用的核心是引用(Reference) ------一种指向其他变量内存地址的指针,通过 & 符号创建。引用本身不持有值的所有权,仅提供临时访问权。Rust 中的引用分为两种,各自有明确的使用规则:
-
不可变引用(&T):最常用的引用类型,用于"只读访问"目标值,不允许修改值的内容。
-
可变引用(&mut T):用于"读写访问"目标值,允许修改值的内容,但有更严格的限制。
无论是哪种引用,都遵循"借用的核心原则":借用不转移所有权,引用失效后,原变量仍可正常使用。
2.1 不可变引用(&T):只读访问,允许多个共存
不可变引用是最安全的借用方式,创建方式为 &变量名,适用于仅需读取值、无需修改的场景。其核心规则:同一时间可以创建多个不可变引用(只读操作不会引发数据竞争)。
示例1:基本的不可变引用使用
rust
fn main() {
let s = String::from("hello borrowing");
// 创建不可变引用 r1,借用 s 的访问权
let r1 = &s;
// 创建不可变引用 r2,允许与 r1 共存
let r2 = &s;
// 正确!通过不可变引用只读访问
println!("r1: {}", r1);
println!("r2: {}", r2);
// 正确!s 的所有权未转移,仍可直接使用
println!("s: {}", s);
}
运行结果:
text
r1: hello borrowing
r2: hello borrowing
s: hello borrowing
这个示例中,r1 和 r2 都是 s 的不可变引用,它们仅获取"读取权",不获取所有权。函数执行过程中,s 始终是值的所有者,引用失效后(超出作用域),s 仍可正常使用。
示例2:函数参数使用不可变引用
回到开篇的痛点示例,我们用不可变引用改造函数,避免所有权转移:
rust
fn main() {
let s = String::from("hello");
// 传递不可变引用 &s,仅借用访问权
print_string(&s);
// 正确!s 的所有权仍在 main 中,可继续使用
println!("s 仍可用:{}", s);
}
// 接收 &String 类型(不可变引用),不获取所有权
fn print_string(s: &String) {
println!("通过引用打印:{}", s);
} // 引用 s 失效,不释放堆内存(无所有权)
运行结果:
text
通过引用打印:hello
s 仍可用:hello
改造后,print_string 函数仅借用 s 的访问权,执行完毕后引用失效,s 的所有权保留在 main 函数中,后续可正常使用------这正是借用的核心价值。
2.2 可变引用(&mut T):可写访问,同一时间仅允许一个
当需要修改借用的值时,需使用可变引用,创建方式为 &mut 变量名。但为了避免数据竞争(多个线程/操作同时读写同一数据,导致结果不确定),Rust 对可变引用施加了严格限制:
-
同一时间,对同一个值,只能有一个可变引用。
-
可变引用与不可变引用不能同时存在(可变引用的写操作会破坏不可变引用的只读预期)。
-
只有"可变变量"(用
mut声明)才能创建可变引用。
示例1:基本的可变引用使用
rust
fn main() {
// 必须声明为可变变量,才能创建可变引用
let mut s = String::from("hello");
// 创建可变引用 r
let r = &mut s;
// 正确!通过可变引用修改值
r.push_str(" world");
println!("通过可变引用修改后:{}", r);
// 正确!s 的所有权仍在 main 中,修改后的值同步生效
println!("s 的值:{}", s);
}
运行结果:
text
通过可变引用修改后:hello world
s 的值:hello world
注意:如果 s 未声明 mut(即不可变变量),创建可变引用会直接编译失败------Rust 不允许通过引用突破变量本身的可变性限制。
示例2:违反可变引用规则的编译错误
以下两种情况都会违反可变引用的规则,导致编译失败,我们通过示例理解"禁止共存"的原因:
rust
fn main() {
let mut s = String::from("test");
// 错误1:同一时间存在两个可变引用
let r1 = &mut s;
// let r2 = &mut s;
// println!("r1: {}, r2: {}", r1, r2);
// 错误2:可变引用与不可变引用同时存在
let r1 = &mut s;
// let r2 = &s;
// println!("r1: {}, r2: {}", r1, r2);
}
为什么这些情况被禁止?核心是为了避免数据竞争:
-
两个可变引用共存:如果两个引用同时修改同一个值,会导致值的最终结果不确定(比如同时给字符串追加内容,可能出现乱码)。
-
可变引用与不可变引用共存:不可变引用预期"值不会被修改",但可变引用可能修改值,破坏这种预期(比如不可变引用读取的是旧值,而可变引用已修改为新值)。
Rust 从编译期禁止这些情况,从根源上保证了数据安全。
示例3:缩小引用作用域,规避冲突
虽然可变引用的限制严格,但我们可以通过"缩小引用的作用域"(用 {} 划分作用域)来规避冲突,让可变引用和不可变引用在不同时间段使用:
rust
fn main() {
let mut s = String::from("hello");
// 作用域1:使用可变引用
{
let r1 = &mut s;
r1.push_str(" rust");
println!("r1: {}", r1);
} // r1 超出作用域,可变引用失效
// 作用域2:使用不可变引用(此时无可变引用,允许创建)
let r2 = &s;
println!("r2: {}", r2);
// 作用域3:再次使用可变引用(r2 已失效,允许创建)
let r3 = &mut s;
r3.push_str("!");
println!("r3: {}", r3);
}
运行结果:
text
r1: hello rust
r2: hello rust
r3: hello rust!
这个示例中,通过 {} 划分不同作用域,让可变引用和不可变引用在"时间上不重叠",既满足了修改需求,又遵守了借用规则------这是 Rust 中规避引用冲突的常用技巧。
三、借用的核心规则(必背)
结合前面的示例,我们总结出 Rust 借用的 4 条核心规则,编译器会强制检查所有代码是否遵守,违反则编译失败:
-
借用不转移所有权,引用仅获取临时访问权,超出作用域后自动失效。
-
不可变引用(&T):允许多个共存,仅支持只读访问,不能修改值。
-
可变引用(&mut T):同一时间仅允许一个存在,支持读写访问;且不能与不可变引用同时存在。
-
只有可变变量(mut 声明)才能创建可变引用,不可变变量无法创建可变引用。
一句话记忆:"不可变多共享,可变独一份;可变与不可变,永远不同存"。
四、借用与函数:传递、返回与悬垂引用
借用在函数中的使用非常频繁,主要涉及"引用作为参数"和"引用作为返回值"两种场景。其中,"返回引用"需要特别注意**悬垂引用(Dangling Reference)**的问题------引用指向的 值已被销毁,导致引用无效。
4.1 引用作为函数参数(借用参数)
除了前面讲的不可变引用参数,我们也可以将可变引用作为函数参数,让函数修改外部变量的值(但不获取所有权):
text
// 接收可变引用,修改字符串内容
fn append_string(s: &mut String, content: &str) {
s.push_str(content);
}
fn main() {
let mut s = String::from("hello");
// 传递可变引用 &mut s
append_string(&mut s, " world");
// 正确!s 的所有权仍在 main 中,修改后的值生效
println!("修改后的 s:{}", s);
}
运行结果:
text
修改后的 s:hello world
这种方式的优势:函数仅临时修改值,不获取所有权,外部变量后续仍可使用,同时避免了值的复制开销。
4.2 引用作为函数返回值(避免悬垂引用)
当函数返回引用时,必须保证"引用指向的值的生命周期,长于引用本身"------否则会产生悬垂引用,编译器会直接禁止这种情况。
错误示例:返回局部变量的引用(悬垂引用)
rust
// 错误!返回局部变量 s 的引用
fn dangling_reference() -> &String {
let s = String::from("dangling");
&s // 试图返回局部变量的引用
} // s 离开作用域,被销毁,堆内存释放
fn main() {
// 此时 ref 是悬垂引用,指向已销毁的值
let ref = dangling_reference();
// println!("{}", ref); // 编译失败
}
上述代码编译失败,原因是:s 是函数 dangling_reference 内的局部变量,函数执行完毕后,s 离开作用域被销毁,堆内存释放。此时返回的引用指向"已不存在的值",即悬垂引用------这会导致内存安全问题,Rust 从编译期直接禁止。
正确示例:返回有效引用(生命周期足够长)
要让返回的引用有效,需保证引用指向的变量"生命周期覆盖引用的使用周期",最常见的做法是"返回函数参数的引用"(参数的生命周期由调用者控制,通常足够长):
rust
// 接收字符串的不可变引用,返回其切片(也是引用)
fn get_slice(s: &String, start: usize, end: usize) -> &str {
&s[start..end]
}
fn main() {
let s = String::from("hello rust");
// 调用函数,返回的引用指向 s 的一部分
let slice = get_slice(&s, 0, 5);
// 正确!s 的生命周期覆盖 slice 的使用周期
println!("切片内容:{}", slice);
}
运行结果:
text
切片内容:hello
这个示例中,返回的引用 &s[start..end] 指向函数参数 s(来自 main 函数的 s),main 函数中的 s 生命周期覆盖了 slice 的使用周期,因此引用有效。
五、特殊的借用:切片(Slice)
切片(Slice)是 Rust 中一种特殊的引用类型,它指向集合(如字符串、数组)的"一部分",而非整个集合。切片本身不持有所有权,仅提供对集合部分内容的只读/读写访问权,是借用机制的典型应用。
切片的核心作用:在不复制数据的前提下,安全地访问集合的子集,避免了因复制带来的性能开销。常见的切片类型有两种:字符串切片(&str)和数组切片(&[T])。
5.1 字符串切片(&str)
字符串切片是最常用的切片类型,用于引用 String 或字面量字符串的一部分,语法为 &字符串[起始索引..结束索引](左闭右开区间,起始索引默认 0,结束索引默认字符串长度)。
rust
fn main() {
let s = String::from("hello world");
// 完整切片(等价于 &s[0..s.len()])
let full_slice = &s[..];
// 从索引 0 到 5(左闭右开),对应 "hello"
let hello = &s[0..5];
// 从索引 6 到末尾(等价于 &s[6..s.len()])
let world = &s[6..];
println!("完整切片:{}", full_slice);
println!("hello 切片:{}", hello);
println!("world 切片:{}", world);
// 字面量字符串本身就是 &str 类型(不可变切片)
let literal = "hello rust";
let rust_slice = &literal[6..];
println!("字面量切片:{}", rust_slice);
}
运行结果:
text
完整切片:hello world
hello 切片:hello
world 切片:world
字面量切片:rust
注意:字符串切片 &str 与 String 的核心区别:
-
String:拥有堆上字符串的所有权,可修改、可增长。 -
&str:是String或字面量字符串的切片(引用),无所有权,只读访问,不可修改。
5.2 数组切片(&[T])
数组切片用于引用数组的一部分,用法与字符串切片类似,语法为 &数组[起始索引..结束索引],支持不可变切片(&[T])和可变切片(&mut [T])。
rust
fn main() {
let mut arr = [10, 20, 30, 40, 50];
// 不可变数组切片:引用索引 1 到 3 的元素([20, 30])
let immutable_slice = &arr[1..3];
println!("不可变切片元素:");
for num in immutable_slice {
println!("{}", num);
}
// 可变数组切片:引用索引 2 到 4 的元素([30, 40, 50])
let mutable_slice = &mut arr[2..5];
// 通过可变切片修改元素
for num in mutable_slice {
*num *= 2; // * 是解引用,用于修改切片指向的元素
}
println!("修改后的数组:{:?}", arr);
}
运行结果:
text
不可变切片元素:
20
30
修改后的数组:[10, 20, 60, 80, 100]
数组切片的优势:可以统一处理数组的任意子集,比如编写一个函数,既能处理整个数组,也能处理数组的一部分:
rust
// 接收数组切片,计算所有元素的和
fn sum_slice(slice: &[i32]) -> i32 {
slice.iter().sum()
}
fn main() {
let arr = [1, 2, 3, 4, 5];
// 处理整个数组(&arr[..] 是完整切片)
let full_sum = sum_slice(&arr[..]);
// 处理数组的一部分(&arr[1..4])
let part_sum = sum_slice(&arr[1..4]);
println!("数组总和:{}", full_sum);
println!("部分元素和:{}", part_sum);
}
运行结果:
text
数组总和:15
部分元素和:9
六、进阶拓展:借用的特殊场景与技巧
6.1 借用与结构体/枚举的结合
结构体和枚举的字段也可以使用引用(借用),但需要注意"字段引用的生命周期必须与结构体/枚举实例的生命周期匹配"(后续文章会详细讲解生命周期,这里先看基础用法):
rust
// 结构体字段使用不可变引用
struct User {
username: &str,
age: u32,
}
fn main() {
// 注意:s 的生命周期必须长于 user 实例
let s = String::from("alice");
let user = User {
username: &s,
age: 28,
};
println!("用户名:{},年龄:{}", user.username, user.age);
} // user 先失效,然后 s 失效,引用有效
如果结构体需要修改字段的值,可以使用可变引用:
rust
struct Data {
value: i32,
}
fn modify_data(data: &mut Data) {
data.value *= 2;
}
fn main() {
let mut data = Data { value: 10 };
// 借用可变引用,修改结构体字段
modify_data(&mut data);
println!("修改后的值:{}", data.value);
}
运行结果:
text
修改后的值:20
6.2 解引用操作(*):通过引用修改值
引用本质是指针,当需要通过可变引用修改"值本身"时,需要使用解引用操作符(*)------它可以"穿透"引用,直接访问或修改引用指向的底层值。
rust
fn main() {
let mut x = 5;
let r = &mut x; // 可变引用
// 解引用 r,修改 x 的值
*r = 10;
println!("x 的值:{}", x); // 输出 10
// 字符串的可变引用解引用
let mut s = String::from("hello");
let r_str = &mut s;
(*r_str).push_str(" world"); // 解引用后修改
println!("s 的值:{}", s); // 输出 hello world
}
注意:对于字符串、结构体等复杂类型,Rust 允许"自动解引用",比如 r_str.push_str(" world") 等价于 (*r_str).push_str(" world"),无需手动解引用,简化了代码。
初学者在使用借用时,容易遇到多种因违反借用规则导致的编译错误。下面补充几个高频错误场景,每个场景均提供「错误代码」「错误原因分析」和「修正代码」,帮助你快速避坑:
初学者在使用借用时,容易遇到以下几种错误,我们总结了对应的解决方法:
错误1:试图修改不可变引用指向的值
核心问题:不可变引用(&T)仅授予只读访问权,无法通过它修改底层值,即使原变量是可变的。
rust
// 错误代码
let mut s = String::from("hello"); // 原变量是可变的
let r = &s; // 但创建的是不可变引用
r.push_str(" world"); // 错误:不可变引用无法修改值
println!("{}", s);
rust
// 修正代码:使用可变引用
let mut s = String::from("hello");
let r = &mut s; // 创建可变引用
r.push_str(" world"); // 正确:可变引用可修改值
println!("{}", s); // 输出:hello world
解决思路:若需修改借用的值,需将引用类型改为可变引用(&mut T),且原变量必须声明为 mut。
错误2:引用作用域超过值的生命周期(悬垂引用)
核心问题:引用指向的 值已被销毁(超出作用域),但引用仍被使用,形成悬垂引用,违反内存安全。
rust
// 错误代码
let r: &String; // 声明一个引用
{
let s = String::from("hello"); // s 在内部作用域创建
r = &s; // 引用指向 s
} // 内部作用域结束,s 被销毁,r 成为悬垂引用
println!("{}", r); // 错误:使用悬垂引用
rust
// 修正代码:保证值的生命周期覆盖引用
let s = String::from("hello"); // s 在外部作用域创建,生命周期更长
let r = &s; // 引用指向 s
println!("{}", r); // 正确:s 未销毁,引用有效
解决思路:调整变量定义位置,确保被引用的值的生命周期,完全覆盖引用的使用周期(值的作用域包含引用的作用域)。
错误3:循环中多次创建可变引用导致冲突
核心问题:初学者易误以为"循环内多次创建可变引用会冲突",实则循环体是独立作用域,迭代结束后引用自动失效,无需额外处理。
rust
// 错误代码(初学者误解版)
let mut s = String::from("hello");
for _ in 0..2 {
let r = &mut s; // 错误认知:认为多次创建可变引用会冲突
r.push_str(" world");
}
println!("{}", s);
rust
// 修正代码(实际可正常运行)
let mut s = String::from("hello");
for _ in 0..2 {
let r = &mut s; // 每次循环创建的可变引用,在迭代结束后自动失效
r.push_str(" world");
}
println!("{}", s); // 输出:hello world world
解决思路:循环体是天然的独立作用域,每次迭代创建的可变引用,会在当前迭代结束后自动失效,不会与下一次迭代的引用冲突,直接使用即可。
rust
// 补充:若在循环外持有引用,才会真正冲突
let mut s = String::from("hello");
let r = &mut s; // 循环外持有可变引用
for _ in 0..2 {
let r2 = &mut s; // 错误:循环内创建的可变引用与 r 冲突
r2.push_str(" world");
}
println!("{}", r);
rust
// 对应修正:缩小循环外引用的作用域
let mut s = String::from("hello");
{
let r = &mut s; // 引用作用域限制在内部块
r.push_str(" first");
} // r 失效
for _ in 0..2 {
let r2 = &mut s; // 可正常创建可变引用
r2.push_str(" world");
}
println!("{}", s); // 输出:hello first world world
七、总结
借用是 Rust 所有权系统的重要补充,其核心价值是"在不转移所有权的前提下,安全地临时访问值",既保证了内存安全,又提升了代码灵活性。本文核心要点总结如下:
-
借用的本质是"引用":通过
&(不可变)或&mut(可变)创建,不转移所有权,仅获取临时访问权。 -
核心规则:不可变引用允许多个共存,可变引用同一时间仅允许一个存在,且可变与不可变引用不能同时存在。
-
函数中的借用:引用作为参数时,避免所有权转移;引用作为返回值时,需避免悬垂引用(保证值的生命周期足够长)。
-
特殊借用:切片(&str / &[T])是指向集合子集的引用,支持只读/读写访问,是高效处理集合的常用工具。
-
常见技巧:通过缩小作用域规避引用冲突,使用解引用(*)修改引用指向的值。
借用的规则看似严格,但都是为了从编译期避免内存安全问题(如数据竞争、悬垂引用)。虽然初期需要适应,但一旦掌握,就能写出既安全又高效的 Rust 代码。后续我们会讲解"生命周期"------它是借用的底层支撑,能帮你解决更复杂的引用生命周期匹配问题。