rust 借用,三巨头之一

在 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

这个示例中,r1r2 都是 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 条核心规则,编译器会强制检查所有代码是否遵守,违反则编译失败:

  1. 借用不转移所有权,引用仅获取临时访问权,超出作用域后自动失效。

  2. 不可变引用(&T):允许多个共存,仅支持只读访问,不能修改值。

  3. 可变引用(&mut T):同一时间仅允许一个存在,支持读写访问;且不能与不可变引用同时存在。

  4. 只有可变变量(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

注意:字符串切片 &strString 的核心区别:

  • 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 所有权系统的重要补充,其核心价值是"在不转移所有权的前提下,安全地临时访问值",既保证了内存安全,又提升了代码灵活性。本文核心要点总结如下:

  1. 借用的本质是"引用":通过 &(不可变)或 &mut(可变)创建,不转移所有权,仅获取临时访问权。

  2. 核心规则:不可变引用允许多个共存,可变引用同一时间仅允许一个存在,且可变与不可变引用不能同时存在。

  3. 函数中的借用:引用作为参数时,避免所有权转移;引用作为返回值时,需避免悬垂引用(保证值的生命周期足够长)。

  4. 特殊借用:切片(&str / &[T])是指向集合子集的引用,支持只读/读写访问,是高效处理集合的常用工具。

  5. 常见技巧:通过缩小作用域规避引用冲突,使用解引用(*)修改引用指向的值。

借用的规则看似严格,但都是为了从编译期避免内存安全问题(如数据竞争、悬垂引用)。虽然初期需要适应,但一旦掌握,就能写出既安全又高效的 Rust 代码。后续我们会讲解"生命周期"------它是借用的底层支撑,能帮你解决更复杂的引用生命周期匹配问题。

相关推荐
小北方城市网1 天前
第 9 课:Python 全栈项目性能优化实战|从「能用」到「好用」(企业级优化方案|零基础落地)
开发语言·数据库·人工智能·python·性能优化·数据库架构
superman超哥1 天前
Rust 内存泄漏检测与防范:超越所有权的内存管理挑战
开发语言·后端·rust·内存管理·rust内存泄漏
悟能不能悟1 天前
java HttpServletRequest 设置header
java·开发语言
云栖梦泽1 天前
易语言运维自动化:中小微企业的「数字化运维瑞士军刀」
开发语言
刘97531 天前
【第23天】23c#今日小结
开发语言·c#
郝学胜-神的一滴1 天前
线程同步:并行世界的秩序守护者
java·linux·开发语言·c++·程序人生
superman超哥1 天前
Rust 移动语义(Move Semantics)的工作原理:零成本所有权转移的深度解析
开发语言·后端·rust·工作原理·深度解析·rust移动语义·move semantics
青茶3601 天前
【js教程】如何用jq的js方法获取url链接上的参数值?
开发语言·前端·javascript
superman超哥1 天前
Rust 所有权转移在函数调用中的表现:编译期保证的零成本抽象
开发语言·后端·rust·函数调用·零成本抽象·rust所有权转移