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 代码。后续我们会讲解"生命周期"------它是借用的底层支撑,能帮你解决更复杂的引用生命周期匹配问题。

相关推荐
Scott9999HH1 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社2 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海2 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖3 小时前
JDK 26 新特性详解
java·开发语言
马优晨4 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
doiito5 小时前
【AI 应用】从“外国人味”到地道中文:kokoroi-rs v0.1.2 架构升级深度解析
ai·rust·系统设计
人邮异步社区5 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大6 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai6 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜6 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript