Rust 可变借用详解:&mut T 与安全修改数据

《Rust 编程实战》系列第36篇

在上一篇文章中,我们学习了Rust的借用Borrow,了解了如何使用不可变引用:

r 复制代码
&T

在不转移所有权的情况下读取数据。例如:

rust 复制代码
fn print_name(name: &str) {
    println!("{}", name);
}
fn main() {
    let name = String::from("Tom");
    print_name(&name);
    println!("{}", name);
}

这里函数只是读取name,并没有取得它的所有权。

但实际开发中,我们不仅需要读取数据,还经常需要通过函数修改原来的数据。例如:

  • 修改用户名

  • 增加用户积分

  • 更新商品库存

  • 向String追加内容

  • 向Vec添加元素

  • 修改结构体状态

    如果只是使用:

r 复制代码
&T

就不能修改数据。

Rust为这种场景提供了:

r 复制代码
&mut T

也就是可变引用,可变借用Mutable Borrow

本文将介绍:

  • 什么是可变借用

  • &mut T基本语法

  • 通过可变引用修改数据

  • 可变借用规则

  • 多个可变引用为什么被禁止

  • 可变引用与不可变引用冲突

  • 借用作用域

  • 函数参数中的&mut

  • String、Vec、Struct可变借用

  • Reborrow重新借用

  • Borrow Checker

  • 常见错误与最佳实践

什么是可变借用

可变借用表示:

临时获得一个值的修改权限,但不取得这个值的所有权。

基本语法:

r 复制代码
&mut T

例如:

ini 复制代码
fn main() {
    let mut number = 10;
    let reference = &mut number;
    *reference = 100;
    println!("{}", number);
}

输出:

复制代码
100

这里:

ini 复制代码
let reference = &mut number;

创建了一个可变引用。

通过:

ini 复制代码
*reference = 100;

修改了原变量number

创建可变引用

要创建可变引用,原变量必须首先使用:

rust 复制代码
mut

声明。

正确:

ini 复制代码
let mut number = 10;
let reference = &mut number;

错误:

ini 复制代码
let number = 10;
let reference = &mut number;

因为number本身不可变。

可以记住:

ini 复制代码
可变变量
+
&mut
=
可变引用

不可变借用与可变借用

不可变借用:

ini 复制代码
let reference = &value;

类型:

r 复制代码
&T

只能读取。

可变借用:

ini 复制代码
let reference = &mut value;

类型:

r 复制代码
&mut T

既可以读取,也可以修改。

例如:

ini 复制代码
fn main() {
    let mut score = 80;
    let score_ref = &mut score;
    *score_ref += 20;
    println!("{}", score);
}

输出:

复制代码
100

为什么修改时需要*

假设:

ini 复制代码
let mut number = 10;
let reference = &mut number;

reference本身不是i32,而是:

rust 复制代码
&mut i32

要修改它指向的真正数据,需要解引用:

ini 复制代码
*reference = 20;

或者:

ini 复制代码
*reference += 10;

这里的:

markdown 复制代码
*

表示访问引用指向的数据。

函数参数中的可变借用

可变借用最常见的场景是函数修改调用者的数据。

例如:

rust 复制代码
fn add_score(score: &mut u32) {
    *score += 10;
}
fn main() {
    let mut score = 90;
    add_score(&mut score);
    println!("{}", score);
}

输出:

复制代码
100

调用:

scss 复制代码
add_score(&mut score);

并没有把score所有权交给函数,而只是临时把修改权限借给函数。

函数结束后:

复制代码
score

仍然属于main

修改String

例如给字符串追加内容:

arduino 复制代码
fn add_suffix(text: &mut String) {
    text.push_str(" Language");
}
fn main() {
    let mut text = String::from("Rust");
    add_suffix(&mut text);
    println!("{}", text);
}

输出:

sql 复制代码
Rust Language

这里不需要:

scss 复制代码
text.clone()

也不需要把String移动进函数再返回。

使用:

rust 复制代码
&mut String

直接修改原字符串更加自然。

字符串参数什么时候使用&str和&mut String

只读取:

php 复制代码
fn print_text(text: &str)

需要修改已有String:

rust 复制代码
fn update_text(text: &mut String)

需要取得所有权:

arduino 复制代码
fn save_text(text: String)

这三种参数表达了三种不同的API意图:

rust 复制代码
&str         临时读取
&mut String  临时修改
String       接管所有权

修改Vec

Vec也经常通过可变借用传入函数:

rust 复制代码
fn add_number(
    numbers: &mut Vec<i32>,
    value: i32,
) {
    numbers.push(value);
}
fn main() {
    let mut numbers = vec![10, 20];
    add_number(&mut numbers, 30);
    println!("{:?}", numbers);
}

输出:

csharp 复制代码
[10, 20, 30]

如果只需要修改现有元素而不需要改变Vec长度,可以使用可变切片:

rust 复制代码
fn double_values(values: &mut [i32]) {
    for value in values {
        *value *= 2;
    }
}
fn main() {
    let mut values = vec![10, 20, 30];
    double_values(&mut values);
    println!("{:?}", values);
}

输出:

csharp 复制代码
[20, 40, 60]

修改结构体

结构体也是可变借用最常见的业务场景。

rust 复制代码
#[derive(Debug)]
struct User {
    name: String,
    score: u32,
}
fn add_user_score(user: &mut User) {
    user.score += 10;
}
fn main() {
    let mut user = User {
        name: String::from("Tom"),
        score: 90,
    };
    add_user_score(&mut user);
    println!("{:?}", user);
}

输出中的积分会变成:

复制代码
100

函数没有取得User所有权,只是修改了其中的数据。

Rust的核心借用规则

Rust借用规则可以简单总结为:

同一时间可以存在多个不可变引用,或者存在一个可变引用。

但不能同时任意混合。

也就是:

r 复制代码
多个&T

可以。

或者:

r 复制代码
一个&mut T

可以。

但同一数据在借用范围重叠时:

r 复制代码
&mut T + &mut T

通常不允许。

r 复制代码
&T + &mut T

通常也不允许。

多个不可变引用是允许的

例如:

ini 复制代码
fn main() {
    let text = String::from("Rust");
    let first = &text;
    let second = &text;
    let third = &text;
    println!("{} {} {}", first, second, third);
}

这是安全的,因为所有引用都只能读取。

同一时间不能有两个重叠使用的可变引用

下面的代码会报错:

ini 复制代码
fn main() {
    let mut text = String::from("Rust");
    let first = &mut text;
    let second = &mut text;
    println!("{} {}", first, second);
}

原因是:

sql 复制代码
first

和:

sql 复制代码
second

都拥有修改同一个数据的能力。

如果允许这样做,就可能出现:

sql 复制代码
first正在修改
second同时修改

导致数据状态不可预测。

因此Rust要求:

对同一数据,在同一个有效借用阶段只能有一个可变访问者。

为什么只能有一个可变借用

假设同时允许:

ini 复制代码
let first = &mut data;
let second = &mut data;

然后:

ini 复制代码
first.push(...);
second.push(...);

如果底层数据发生扩容、重新分配或者多个线程并发访问,就可能产生:

  • 数据竞争

  • 悬空引用

  • 内存错误

    Rust通过编译器提前禁止这种代码。

    这也是Rust能够安全并发的重要基础。

不可变借用与可变借用不能重叠

错误示例:

ini 复制代码
fn main() {
    let mut text = String::from("Rust");
    let first = &text;
    let second = &mut text;
    println!("{} {}", first, second);
}

first认为数据在自己读取期间不会被修改。

second拥有修改能力。

两者同时存在会破坏这个保证,因此编译器拒绝。

借用作用域

现代Rust并不是简单地认为引用一定存活到代码块结束,而通常会根据引用的最后一次使用判断借用什么时候结束。

例如:

arduino 复制代码
fn main() {
    let mut text = String::from("Rust");
    let reference = &text;
    println!("{}", reference);
    let mutable = &mut text;
    mutable.push('!');
    println!("{}", mutable);
}

这段代码通常可以工作。

因为不可变引用reference最后一次使用是在:

arduino 复制代码
println!("{}", reference);

之后,它不再使用,因此不可变借用已经结束。

接下来可以创建可变借用。

可变借用结束后可以再次借用

例如:

ini 复制代码
fn main() {
    let mut text = String::from("Rust");
    {
        let first = &mut text;
        first.push('!');
    }
    let second = &mut text;
    second.push('?');
    println!("{}", text);
}

输出:

复制代码
Rust!?

第一个可变引用离开内部作用域后,可以再次创建新的可变引用。

Reborrow重新借用

可变引用还可以被重新借用。

例如:

rust 复制代码
fn append(text: &mut String) {
    text.push('!');
}
fn main() {
    let mut text = String::from("Rust");
    let reference = &mut text;
    append(&mut *reference);
    reference.push('?');
    println!("{}", reference);
}

这里:

rust 复制代码
&mut *reference

从原可变引用中临时创建了一个新的可变借用,这种行为通常称为:

复制代码
Reborrow

实际上很多函数调用中Rust会自动完成重新借用,因此经常可以直接写:

scss 复制代码
append(reference);

并在适当情况下继续使用reference

可变引用本身也可以修改

如果一个变量保存引用,并且希望让这个变量之后指向不同数据,需要区分:

r 复制代码
&mut T

和:

rust 复制代码
mut reference

例如:

ini 复制代码
fn main() {
    let first = 10;
    let second = 20;
    let mut reference = &first;
    println!("{}", reference);
    reference = &second;
    println!("{}", reference);
}

这里reference变量本身可变,所以它可以重新指向另一个值。

但它的类型仍然是:

rust 复制代码
&i32

不能通过它修改firstsecond

而:

ini 复制代码
let reference = &mut first;

表示引用指向的数据可以修改。

两者不要混淆。

返回可变引用

函数也可以返回可变引用,例如:

rust 复制代码
fn first_mut(
    values: &mut [i32],
) -> Option<&mut i32> {
    values.first_mut()
}
fn main() {
    let mut values = [10, 20, 30];
    if let Some(value) = first_mut(&mut values) {
        *value = 100;
    }
    println!("{:?}", values);
}

输出:

csharp 复制代码
[100, 20, 30]

这里返回的引用仍然借用自原来的values

这种关系会涉及生命周期,下一篇会正式讲解。

Vec迭代修改

可以使用:

scss 复制代码
iter_mut()

获取元素的可变引用。

typescript 复制代码
fn main() {
    let mut numbers = vec![1, 2, 3, 4];
    for number in numbers.iter_mut() {
        *number *= 10;
    }
    println!("{:?}", numbers);
}

输出:

csharp 复制代码
[10, 20, 30, 40]

也可以直接:

rust 复制代码
for number in &mut numbers {
    *number *= 10;
}

HashMap可变借用

HashMap可以使用:

scss 复制代码
get_mut()

获取某个值的可变引用。

rust 复制代码
use std::collections::HashMap;
fn main() {
    let mut scores = HashMap::new();
    scores.insert("Tom", 90);
    if let Some(score) = scores.get_mut("Tom") {
        *score += 10;
    }
    println!("{:?}", scores);
}

输出:

json 复制代码
{"Tom": 100}

这不会取出值的所有权,而是直接修改HashMap中的数据。

可变借用与push常见冲突

下面是很经典的错误:

ini 复制代码
fn main() {
    let mut numbers = vec![10, 20, 30];
    let first = &numbers[0];
    numbers.push(40);
    println!("{}", first);
}

为什么可能报错?

因为:

sql 复制代码
first

借用了Vec内部元素。

而:

ini 复制代码
numbers.push(40);

可能导致Vec扩容,原来的内存地址可能改变。

如果扩容后继续使用first,它可能指向旧地址。

Rust编译器因此禁止这种潜在危险。

正确调整借用范围

如果先使用引用,再修改Vec:

ini 复制代码
fn main() {
    let mut numbers = vec![10, 20, 30];
    let first = &numbers[0];
    println!("{}", first);
    numbers.push(40);
    println!("{:?}", numbers);
}

这里firstpush()前已经最后一次使用,因此借用可以结束,后面可以安全修改Vec。

实战:商品库存修改

rust 复制代码
#[derive(Debug)]
struct Product {
    name: String,
    stock: u32,
}
fn buy(
    product: &mut Product,
    count: u32,
) -> Result<(), String> {
    if product.stock < count {
        return Err(String::from("库存不足"));
    }
    product.stock -= count;
    Ok(())
}
fn main() {
    let mut product = Product {
        name: String::from("Rust Book"),
        stock: 10,
    };
    match buy(&mut product, 3) {
        Ok(()) => println!("购买成功"),
        Err(error) => println!("{}", error),
    }
    println!("剩余库存:{}", product.stock);
}

函数需要修改库存,但不应该取得整个商品所有权,因此:

rust 复制代码
&mut Product

非常适合这个场景。

实战:用户名标准化

ini 复制代码
fn normalize_name(name: &mut String) {
    let normalized = name
        .trim()
        .to_lowercase();
    *name = normalized;
}
fn main() {
    let mut name = String::from("  TOM  ");
    normalize_name(&mut name);
    println!("{}", name);
}

输出:

复制代码
tom

这里函数直接修改调用者持有的字符串。

常见错误

原变量没有mut

错误:

ini 复制代码
let text = String::from("Rust");
let reference = &mut text;

正确:

ini 复制代码
let mut text = String::from("Rust");
let reference = &mut text;

同时创建两个可变引用

错误:

ini 复制代码
let first = &mut value;
let second = &mut value;
println!("{} {}", first, second);

同一借用阶段不能拥有两个重叠的可变引用。

同时存在可变与不可变借用

错误:

ini 复制代码
let read = &value;
let write = &mut value;
println!("{} {}", read, write);

应缩短其中一个借用的使用范围。

可变借用期间直接访问原变量

例如:

arduino 复制代码
let reference = &mut text;
println!("{}", text);
reference.push('!');

此时reference后面仍然会使用,因此借用仍有效,直接访问text会造成冲突。

为了修改而取得所有权

不推荐:

sql 复制代码
fn update(mut user: User) -> User

如果只是修改原对象,更自然:

sql 复制代码
fn update(user: &mut User)

可变借用最佳实践

只读使用&T

例如:

sql 复制代码
fn show(user: &User)

修改但不接管使用&mut T

例如:

sql 复制代码
fn update(user: &mut User)

需要长期保存时取得所有权

例如:

php 复制代码
fn save(user: User)

尽量缩短可变借用范围

可变借用的权限很强,范围越小,越不容易与其他访问发生冲突。

优先借用具体需要的数据

如果函数只需要修改结构体一个字段,可以考虑直接传字段引用,而不是把整个大型对象交给函数。

理解编译错误背后的原因

遇到:

csharp 复制代码
cannot borrow as mutable
cannot borrow ... more than once
cannot borrow as mutable because it is also borrowed as immutable

不要第一时间使用clone()绕过去,应先检查借用范围和数据访问设计。

本章小结

可变借用允许Rust在不转移所有权的情况下安全修改数据。

本文学习了:

  • 使用&mut T创建可变引用

  • 原变量必须声明为mut

  • 使用*修改引用指向的数据

  • 函数可以通过&mut T修改调用者的数据

  • String、Vec、HashMap和Struct都可以可变借用

  • 同一有效借用阶段通常只能有一个可变引用

  • 可变借用和不可变借用不能重叠冲突

  • 借用会根据最后一次使用的位置结束

  • 可变引用可以重新借用Reborrow

  • iter_mut()可以遍历并修改Vec元素

  • Vec扩容与引用之间可能产生借用冲突

  • Borrow Checker在编译阶段保证引用安全

    可以记住:

&T表示"借给我读取"。

&mut T表示"借给我修改"。

所有权仍然属于原变量,函数只是临时获得访问权限。

Rust限制可变引用数量,是为了从编译阶段消除数据竞争和悬空引用。

下一篇预告

下一篇我们将进入Rust所有权系统中另一个非常重要的概念:

Rust 生命周期基础 Lifetime 详解

内容包括:

  • 什么是生命周期

  • 为什么引用需要生命周期

  • 悬空引用问题

  • Borrow Checker如何判断引用有效性

  • 生命周期与作用域的关系

  • 生命周期参数'a

  • 函数返回引用

  • 多个引用参数

  • 生命周期省略规则

  • 结构体中的引用与生命周期

  • 'static生命周期

  • 生命周期不是延长变量寿命

  • 常见生命周期编译错误与实战案例

相关推荐
程序员爱钓鱼1 小时前
Go 类型转换详解
后端·面试·go
mldong9 小时前
工作流引擎的灵魂:状态机与 submitType
后端
码匠许师傅9 小时前
【C++ 面试真题】C++ 的 const 和 constexpr 有什么区别?
c++·面试
朦胧之11 小时前
Go 基础
后端·go
lun11 小时前
Agent 工具调用成长史:理清楚 Function Calling、MCP、CLI 的关系
后端
Python私教11 小时前
Codex 自动写 Commit 够安全吗?一套证据化 Git 提交流程
人工智能·git·后端
Python私教12 小时前
AI Agent 上生产要不要开写权限?我把执行链拆成 4 道闸门
人工智能·后端·python
lun12 小时前
Claude Code 多 Agent 实现:subAgent & Agent Teams
后端
不负岁月无痕13 小时前
简单理解操作系统结构
java·linux·c语言·开发语言·c++·面试