《Rust 编程实战》系列第36篇
在上一篇文章中,我们学习了Rust的借用Borrow,了解了如何使用不可变引用:
&T
在不转移所有权的情况下读取数据。例如:
fn print_name(name: &str) {
println!("{}", name);
}
fn main() {
let name = String::from("Tom");
print_name(&name);
println!("{}", name);
}
这里函数只是读取name,并没有取得它的所有权。
但实际开发中,我们不仅需要读取数据,还经常需要通过函数修改原来的数据。例如:
-
修改用户名
-
增加用户积分
-
更新商品库存
-
向String追加内容
-
向Vec添加元素
-
修改结构体状态
如果只是使用:
&T
就不能修改数据。
Rust为这种场景提供了:
&mut T
也就是可变引用,可变借用Mutable Borrow。
本文将介绍:
-
什么是可变借用
-
&mut T基本语法 -
通过可变引用修改数据
-
可变借用规则
-
多个可变引用为什么被禁止
-
可变引用与不可变引用冲突
-
借用作用域
-
函数参数中的
&mut -
String、Vec、Struct可变借用
-
Reborrow重新借用
-
Borrow Checker
-
常见错误与最佳实践
什么是可变借用
可变借用表示:
临时获得一个值的修改权限,但不取得这个值的所有权。
基本语法:
&mut T
例如:
fn main() {
let mut number = 10;
let reference = &mut number;
*reference = 100;
println!("{}", number);
}
输出:
100
这里:
let reference = &mut number;
创建了一个可变引用。
通过:
*reference = 100;
修改了原变量number。
创建可变引用
要创建可变引用,原变量必须首先使用:
mut
声明。
正确:
let mut number = 10;
let reference = &mut number;
错误:
let number = 10;
let reference = &mut number;
因为number本身不可变。
可以记住:
可变变量
+
&mut
=
可变引用
不可变借用与可变借用
不可变借用:
let reference = &value;
类型:
&T
只能读取。
可变借用:
let reference = &mut value;
类型:
&mut T
既可以读取,也可以修改。
例如:
fn main() {
let mut score = 80;
let score_ref = &mut score;
*score_ref += 20;
println!("{}", score);
}
输出:
100
为什么修改时需要*
假设:
let mut number = 10;
let reference = &mut number;
reference本身不是i32,而是:
&mut i32
要修改它指向的真正数据,需要解引用:
*reference = 20;
或者:
*reference += 10;
这里的:
*
表示访问引用指向的数据。
函数参数中的可变借用
可变借用最常见的场景是函数修改调用者的数据。
例如:
fn add_score(score: &mut u32) {
*score += 10;
}
fn main() {
let mut score = 90;
add_score(&mut score);
println!("{}", score);
}
输出:
100
调用:
add_score(&mut score);
并没有把score所有权交给函数,而只是临时把修改权限借给函数。
函数结束后:
score
仍然属于main。
修改String
例如给字符串追加内容:
fn add_suffix(text: &mut String) {
text.push_str(" Language");
}
fn main() {
let mut text = String::from("Rust");
add_suffix(&mut text);
println!("{}", text);
}
输出:
Rust Language
这里不需要:
text.clone()
也不需要把String移动进函数再返回。
使用:
&mut String
直接修改原字符串更加自然。
字符串参数什么时候使用&str和&mut String
只读取:
fn print_text(text: &str)
需要修改已有String:
fn update_text(text: &mut String)
需要取得所有权:
fn save_text(text: String)
这三种参数表达了三种不同的API意图:
&str 临时读取
&mut String 临时修改
String 接管所有权
修改Vec
Vec也经常通过可变借用传入函数:
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);
}
输出:
[10, 20, 30]
如果只需要修改现有元素而不需要改变Vec长度,可以使用可变切片:
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);
}
输出:
[20, 40, 60]
修改结构体
结构体也是可变借用最常见的业务场景。
#[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借用规则可以简单总结为:
同一时间可以存在多个不可变引用,或者存在一个可变引用。
但不能同时任意混合。
也就是:
多个&T
可以。
或者:
一个&mut T
可以。
但同一数据在借用范围重叠时:
&mut T + &mut T
通常不允许。
&T + &mut T
通常也不允许。
多个不可变引用是允许的
例如:
fn main() {
let text = String::from("Rust");
let first = &text;
let second = &text;
let third = &text;
println!("{} {} {}", first, second, third);
}
这是安全的,因为所有引用都只能读取。
同一时间不能有两个重叠使用的可变引用
下面的代码会报错:
fn main() {
let mut text = String::from("Rust");
let first = &mut text;
let second = &mut text;
println!("{} {}", first, second);
}
原因是:
first
和:
second
都拥有修改同一个数据的能力。
如果允许这样做,就可能出现:
first正在修改
second同时修改
导致数据状态不可预测。
因此Rust要求:
对同一数据,在同一个有效借用阶段只能有一个可变访问者。
为什么只能有一个可变借用
假设同时允许:
let first = &mut data;
let second = &mut data;
然后:
first.push(...);
second.push(...);
如果底层数据发生扩容、重新分配或者多个线程并发访问,就可能产生:
-
数据竞争
-
悬空引用
-
内存错误
Rust通过编译器提前禁止这种代码。
这也是Rust能够安全并发的重要基础。
不可变借用与可变借用不能重叠
错误示例:
fn main() {
let mut text = String::from("Rust");
let first = &text;
let second = &mut text;
println!("{} {}", first, second);
}
first认为数据在自己读取期间不会被修改。
而second拥有修改能力。
两者同时存在会破坏这个保证,因此编译器拒绝。
借用作用域
现代Rust并不是简单地认为引用一定存活到代码块结束,而通常会根据引用的最后一次使用判断借用什么时候结束。
例如:
fn main() {
let mut text = String::from("Rust");
let reference = &text;
println!("{}", reference);
let mutable = &mut text;
mutable.push('!');
println!("{}", mutable);
}
这段代码通常可以工作。
因为不可变引用reference最后一次使用是在:
println!("{}", reference);
之后,它不再使用,因此不可变借用已经结束。
接下来可以创建可变借用。
可变借用结束后可以再次借用
例如:
fn main() {
let mut text = String::from("Rust");
{
let first = &mut text;
first.push('!');
}
let second = &mut text;
second.push('?');
println!("{}", text);
}
输出:
Rust!?
第一个可变引用离开内部作用域后,可以再次创建新的可变引用。
Reborrow重新借用
可变引用还可以被重新借用。
例如:
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);
}
这里:
&mut *reference
从原可变引用中临时创建了一个新的可变借用,这种行为通常称为:
Reborrow
实际上很多函数调用中Rust会自动完成重新借用,因此经常可以直接写:
append(reference);
并在适当情况下继续使用reference。
可变引用本身也可以修改
如果一个变量保存引用,并且希望让这个变量之后指向不同数据,需要区分:
&mut T
和:
mut reference
例如:
fn main() {
let first = 10;
let second = 20;
let mut reference = &first;
println!("{}", reference);
reference = &second;
println!("{}", reference);
}
这里reference变量本身可变,所以它可以重新指向另一个值。
但它的类型仍然是:
&i32
不能通过它修改first或second。
而:
let reference = &mut first;
表示引用指向的数据可以修改。
两者不要混淆。
返回可变引用
函数也可以返回可变引用,例如:
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);
}
输出:
[100, 20, 30]
这里返回的引用仍然借用自原来的values。
这种关系会涉及生命周期,下一篇会正式讲解。
Vec迭代修改
可以使用:
iter_mut()
获取元素的可变引用。
fn main() {
let mut numbers = vec![1, 2, 3, 4];
for number in numbers.iter_mut() {
*number *= 10;
}
println!("{:?}", numbers);
}
输出:
[10, 20, 30, 40]
也可以直接:
for number in &mut numbers {
*number *= 10;
}
HashMap可变借用
HashMap可以使用:
get_mut()
获取某个值的可变引用。
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);
}
输出:
{"Tom": 100}
这不会取出值的所有权,而是直接修改HashMap中的数据。
可变借用与push常见冲突
下面是很经典的错误:
fn main() {
let mut numbers = vec![10, 20, 30];
let first = &numbers[0];
numbers.push(40);
println!("{}", first);
}
为什么可能报错?
因为:
first
借用了Vec内部元素。
而:
numbers.push(40);
可能导致Vec扩容,原来的内存地址可能改变。
如果扩容后继续使用first,它可能指向旧地址。
Rust编译器因此禁止这种潜在危险。
正确调整借用范围
如果先使用引用,再修改Vec:
fn main() {
let mut numbers = vec![10, 20, 30];
let first = &numbers[0];
println!("{}", first);
numbers.push(40);
println!("{:?}", numbers);
}
这里first在push()前已经最后一次使用,因此借用可以结束,后面可以安全修改Vec。
实战:商品库存修改
#[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);
}
函数需要修改库存,但不应该取得整个商品所有权,因此:
&mut Product
非常适合这个场景。
实战:用户名标准化
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
错误:
let text = String::from("Rust");
let reference = &mut text;
正确:
let mut text = String::from("Rust");
let reference = &mut text;
同时创建两个可变引用
错误:
let first = &mut value;
let second = &mut value;
println!("{} {}", first, second);
同一借用阶段不能拥有两个重叠的可变引用。
同时存在可变与不可变借用
错误:
let read = &value;
let write = &mut value;
println!("{} {}", read, write);
应缩短其中一个借用的使用范围。
可变借用期间直接访问原变量
例如:
let reference = &mut text;
println!("{}", text);
reference.push('!');
此时reference后面仍然会使用,因此借用仍有效,直接访问text会造成冲突。
为了修改而取得所有权
不推荐:
fn update(mut user: User) -> User
如果只是修改原对象,更自然:
fn update(user: &mut User)
可变借用最佳实践
只读使用&T
例如:
fn show(user: &User)
修改但不接管使用&mut T
例如:
fn update(user: &mut User)
需要长期保存时取得所有权
例如:
fn save(user: User)
尽量缩短可变借用范围
可变借用的权限很强,范围越小,越不容易与其他访问发生冲突。
优先借用具体需要的数据
如果函数只需要修改结构体一个字段,可以考虑直接传字段引用,而不是把整个大型对象交给函数。
理解编译错误背后的原因
遇到:
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生命周期 -
生命周期不是延长变量寿命
-
常见生命周期编译错误与实战案例