Rust 语言从入门到实战 唐刚
基础篇 (11讲)
03|所有权(下):Rust中借用与引用的规则
上节课:
- 计算机内存结构
- Rust 在内存资源管理上特立独行的设计——所有权
- Rust 用所有权来重构整个软件体系。
借用与引用
上节最后一个例子。想在函数 foo 执行后继续使用字符串 s1,通过把字符串的所有权转移出来,来达到目的。
fn foo(s: String) -> String {
println!("{s}");
s
}
fn main() {
let s1 = String::from("I am a superman.");
let s1 = foo(s1);
println!("{s1}");
}
可以,但麻烦、冗余。
Rust 中,借用和引用是一体两面。你把东西借给别人用,就是别人持有了这个东西的引用。会混用这两个词。
Rust 中,变量前加“&”符号表示引用,如 &x。
引用是一种值,是固定尺寸的值,与 CPU 位数一致( 64 位或 32 位)。是值,可以赋给另一个变量。(固定且小尺寸的值)赋值时,就直接复制一份这个引用。
使用引用。
fn main() {
let a = 10u32;
let b = &a; // b是变量a的一级引用
let c = &&&&&a; // c是变量a的多级引用
let d = &b; // d是变量a的间接引用
let e = b; // 引用b再赋值给e
println!("{a}");
println!("{b}");
println!("{c}");
println!("{d}");
println!("{e}");
}
// 输出
10
10
10
10
10
Rust 识别了意图,不打印引用的内存地址,打印了被引用对象的值。
与 C 这种纯底层语言的区别显著,Rust 对程序员更友好,面向业务。关注最终那个值,而不是中间过程的内存地址。
b 和 e 都是对 a 的一级引用。引用是固定尺寸的值,let e = b 就是引用的复制,没有再复制一份 a 的值。
对字符串会怎样?
fn main() {
let s1 = String::from("I am a superman.");
let s2 = &s1;
let s3 = &&&&&s1;
let s4 = &s2;
let s5 = s2;
println!("{s1}");
println!("{s2}");
println!("{s3}");
println!("{s4}");
println!("{s5}");
}
// 输出
I am a superman.
I am a superman.
I am a superman.
I am a superman.
I am a superman.
符合期望。这些引用都没有导致堆中的字符串资源被复制一份或多份。字符串的所有权仍在 s1 那里,s2、s3、s4、s5 都是对这个所有权变量的引用。可将变量按新的维度划分为所有权型变量和引用型变量。
Rust 中,所有权型变量(如 s1)带有值和类型信息,引用型变量(如 s2、s3、s4、s5)也带有值和类型信息,不然没法正确回溯到最终的值。这些信息由 Rust 编译器维护。
不可变引用、可变引用
Rust 变量具有可变性,引用也具有可变性,--> Rust 设计一致性。
&x 不可变引用。 &mut x 可变引用
- 引用分成不可变引用和可变引用。
- &x 是对变量 x 的不可变引用。
- &mut x 是对变量 x 的可变引用。
mut 和 x 中间有个空格,避免和 &mutx 混淆。
把书借给别人,只能看,不能在书上记笔记,是不可变引用。若允许在书上写写划划,是可变引用。
要对一个变量内容进行修改,必须拥有所有权型变量才行。引用别人的库,它没有把所有权类型暴露出来,但确实又有更新其内部状态的需求。既是一种引用,又能够修改指向资源的内容。就引入了可变引用。
前面引用的例子,只访问(打印)变量的值,没有修改,所以没问题。
用引用修改变量的值:
fn main() {
let a = 10u32;
let b = &mut a;
*b = 20;
println!("{b}");
}
// 提示
error[E0596]: cannot borrow `a` as mutable, as it is not declared as mutable
--> src/main.rs:19:13
|
19 | let b = &mut a;
| ^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
18 | let mut a = 10u32;
| +++
要修改一个变量的值,变量名前要加 mut 修饰符,忘加了。
fn main() {
let mut a = 10u32;
let b = &mut a;
*b = 20;
println!("{b}");
}
// 输出
20
改动一下。
fn main() {
let mut a = 10u32;
let b = &mut a;
*b = 20;
println!("{b}");
println!("{a}"); // 这里多打印了一行a
}
// 输出
20
20
正确输出了修改后的值。
换一下两个打印语句的位置。
fn main() {
let mut a = 10u32;
let b = &mut a;
*b = 20;
println!("{a}"); // 这一句移到前面来
println!("{b}");
}
// 编译居然报错了!
Compiling playground v0.0.1 (/playground)
error[E0502]: cannot borrow `a` as immutable because it is also borrowed as mutable
--> src/main.rs:6:15
|
3 | let b = &mut a;
| ------ mutable borrow occurs here
...
6 | println!("{a}"); // 这一句移到的前面来
| ^^^ immutable borrow occurs here
// 提示说这里发生了不可变借用
7 | println!("{b}");
| --- mutable borrow later used here
// 在这后面使用了可变借用
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
只移动了一下打印语句,会导致编译问题。什么道理!-->初学 Rust 的心情。
为什么?
- 打印语句 println! ,不管传所有权型变量还是引用型变量,都能打印出预期的值。实际上 println! 中默认会对所有权变量做不可变借用操作(第 6 行)。
- 可变引用调用的时机(第 7 行)和不可变引用调用的时机(第 6 行),好像有顺序要求。
设计另一个例子。
fn main() {
let mut a = 10u32;
let b = &mut a;
*b = 20;
let c = &a; // 在利用b更新了a的值后,c再次借用a
}
可顺利编译。
但加了一句打印又不行了!
fn main() {
let mut a = 10u32;
let b = &mut a;
*b = 20;
let c = &a; // 在利用b更新了a的值后,c再次借用a
println!("{b}"); // 加了一句打印语句
}
// 提示
Compiling playground v0.0.1 (/playground)
error[E0502]: cannot borrow `a` as immutable because it is also borrowed as mutable
// 不能将a借用为不可变的,因为它已经被可变借用了
--> src/main.rs:5:13
|
3 | let b = &mut a;
| ------ mutable borrow occurs here
// 可变借用发生在这里
4 | *b = 20;
5 | let c = &a;
| ^^ immutable borrow occurs here
// 不可变借用发生在这里
6 |
7 | println!("{b}"); // 加了一句打印语句
| --- mutable borrow later used here
// 可变借用在这里使用了
why?改一下打印语句。
fn main() {
let mut a = 10u32;
let b = &mut a;
*b = 20;
let c = &a;
println!("{c}"); // 不打印b了,换成打印c
}
// 输出
20
编译通过,打印出 20。
尝试把变量 c 的定义移到前面,又不能编译了。
fn main() {
let mut a = 10u32;
let c = &a; // c的定义移到这里来了
let b = &mut a;
*b = 20;
println!("{c}");
}
// 提示
Compiling playground v0.0.1 (/playground)
error[E0502]: cannot borrow `a` as mutable because it is also borrowed as immutable
--> src/main.rs:4:13
|
3 | let c = &a; // c的定义移到这里来了
| -- immutable borrow occurs here
4 | let b = &mut a;
| ^^^^^^ mutable borrow occurs here
...
7 | println!("{c}");
| --- immutable borrow later used here
Rust 就像一头发疯的野牛!摸清它的脾气,驯服它!
再尝试修改代码,又编译通过了。
fn main() {
let mut a = 10u32;
let c = &a; // c的定义移到这里来了
let b = &mut a;
*b = 20;
println!("{b}"); // 这里打印的变量换成b
}
什么规律?引用的最后一次调用时机很关键。
- 所有权型变量的作用域:从定义开始,到花括号结束。
- 引用型变量的作用域:从定义起,到最后一次使用时结束。
示例中,所有权型变量 a 的作用域是 2~8 行;不可变引用 c 的作用域只有第 3 行(定义了但没被使用);可变引用 b 的作用域是 4~7 行。
一个所有权型变量的可变引用与不可变引用的作用域不能交叠,即不能同时存在。用这条规则分析前面的示例。
fn main() {
let mut a = 10u32;
let c = &a;
let b = &mut a;
*b = 20;
println!("{c}");
}
所有权型变量 a 的作用域是 2~8 行,不可变引用 c 的作用域是 3~7 行,可变引用 b 的作用域是 4~5 行。b 和 c 的作用域交叠了,无法编译通过。
再一个例子。
fn main() {
let mut a = 10u32;
let b = &mut a;
*b = 20;
let d = &mut a;
println!("{d}"); // 打印d
}
// 输出
20
打印出 20。尝试打印 b 试试。
fn main() {
let mut a = 10u32;
let b = &mut a;
*b = 20;
let d = &mut a;
println!("{b}"); // 打印b
}
// 编译不通过,提示:
Compiling playground v0.0.1 (/playground)
error[E0499]: cannot borrow `a` as mutable more than once at a time
// 在一个时刻不能把`a`以可变借用形式借用超过一次
--> src/main.rs:5:13
|
3 | let b = &mut a;
| ------ first mutable borrow occurs here
4 | *b = 20;
5 | let d = &mut a;
| ^^^^^^ second mutable borrow occurs here
6 |
7 | println!("{b}");
| --- first borrow later used here
编译器抱怨:“在一个时刻不能把 a 以可变借用形式借用超过一次”。b 的作用域是 3~7 行,d 的作用域是第 5 行,难怪会报错。同一个所有权型变量的可变借用之间的作用域也不能交叠。
继续:
fn main() {
let mut a = 10u32;
let r1 = &a;
a = 20;
println!("{r1}");
}
// 编译报错:
Compiling playground v0.0.1 (/playground)
error[E0506]: cannot assign to `a` because it is borrowed
// 不能给a赋值,因为它被借用了
--> src/main.rs:4:5
|
3 | let r1 = &a;
| -- `a` is borrowed here
4 | a = 20;
| ^^^^^^ `a` is assigned to here but it was already borrowed
5 |
6 | println!("{r1}");
| ---- borrow later used here
提示在有借用的情况下,不能对所有权变量进行更改值的操作(写操作)。
有可变借用存在的情况下也一样。
fn main() {
let mut a = 10u32;
let r1 = &mut a;
a = 20;
println!("{r1}");
}
// 编译报错:
Compiling playground v0.0.1 (/playground)
error[E0506]: cannot assign to `a` because it is borrowed
--> src/main.rs:4:5
|
3 | let r1 = &mut a;
| ------ `a` is borrowed here
4 | a = 20;
| ^^^^^^ `a` is assigned to here but it was already borrowed
5 |
6 | println!("{r1}");
| ---- borrow later used here
提示在有借用的情况下,不能对所有权变量进行更改值的操作(写操作)。
阶段性的总结,得出关于引用(借用)的一些规则。
- 所有权型变量的作用域:从定义时开始,到所属那层花括号结束。
- 引用型变量的作用域:从定义起,到它最后一次使用时结束。
- 引用(不可变引用和可变引用)型变量的作用域不会长于所有权变量的作用域。--> 悬锤引用,典型的内存安全问题。
- 一个所有权型变量的不可变引用可以同时存在多个,可以复制多份。
- 一个所有权型变量的可变引用与不可变引用的作用域不能交叠,即不能同时存在。
- 某个时刻对某个所有权型变量只能存在一个可变引用,不能有超过一个可变借用同时存在,即,对同一个所有权型变量的可变借用之间的作用域不能交叠。
- 在有借用存在的情况下,不能通过原所有权型变量对值进行更新。当借用完成后(借用的作用域结束后),物归原主,又可以使用所有权型变量对值做更新操作了。
试试可变引用能否被复制:
fn main() {
let mut a = 10u32;
let r1 = &mut a;
let r2 = r1;
println!("{r1}")
}
// 出错了,提示:
error[E0382]: borrow of moved value: `r1`
--> src/main.rs:6:16
|
3 | let r1 = &mut a;
| -- move occurs because `r1` has type `&mut u32`, which does not implement the `Copy` trait
4 | let r2 = r1;
| -- value moved here
5 |
6 | println!("{r1}")
| ^^ value borrowed here after move
说 r1 的值移动给了 r2,r1 不能再用了。
修改一下例子。
fn main() {
let mut a = 10u32;
let r1 = &mut a;
let r2 = r1;
println!("{r2}"); // 打印r2
}
// 输出
10
成功打印。
可以看出,可变引用的再赋值,会执行移动操作。赋值后,原来那个可变引用变量就不能用了。类似于所有权的转移,因此一个所有权型变量的可变引用也具有所有权特征,可被理解为那个所有权变量的独家代理,具有排它性。
多级引用
看剩下的一些语言细节。代码展示了 mut 修饰符,&mut 和 & 同时出现的情况。
fn main() {
let mut a1 = 10u32;
let mut a2 = 15u32;
let mut b = &mut a1;
b = &mut a2;
let mut c = &a1;
c = &a2;
}
一个多级可变引用的例子。
fn main() {
let mut a1 = 10u32;
let mut b = &mut a1;
*b = 20;
let c = &mut b;
**c = 30; // 多级解引用操作
println!("{c}");
}
// 输出
30
若解引用错误会怎样?
fn main() {
let mut a1 = 10u32;
let mut b = &mut a1;
*b = 20;
let c = &mut b;
*c = 30; // 这里对二级可变引用只使用一级解引用操作
println!("{c}");
}
// 哦!会报错。
Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
--> src/main.rs:7:10
|
7 | *c = 30;
| -- ^^ expected `&mut u32`, found integer
| |
| expected due to the type of this binding
|
help: consider dereferencing here to assign to the mutably borrowed value
|
7 | **c = 30;
| +
正确识别到了中间引用的类型为 &mut u32,而要给它赋值为 u32,一定是代码写错了,还建议了正确的写法。强大!
再一个例子。
fn main() {
let mut a1 = 10u32;
let b = &mut a1;
let mut c = &b;
let d = &mut c;
***d = 30;
println!("{d}");
}
// 提示:
error[E0594]: cannot assign to `***d`, which is behind a `&` reference
--> src/main.rs:21:5
|
21 | ***d = 30;
| ^^^^^^^^^ cannot assign
For more information about this error, try `rustc --explain E0594`.
提示:不能这样更新目标的值,因为目标躲在一个 & 引用后面。
又发现 Rust 中三条关于引用的知识点。
- 对于多级可变引用,要用可变引用去修改目标资源的值,需要做正确的多级解引用操作,如例子中 **c,做了两级解引用。
- 只有全是多级可变引用的情况下,才能修改到目标资源的值。
- 对于多级引用(包含可变和不可变),打印语句中,可以自动为我们解引用正确的层数,直到访问到目标资源的值,符合人的直觉和业务需求。
用引用改进函数的定义
用 引用,改进前面将字符串所有权传进函数,然后又传出来的例子。
将字符串的不可变引用传进函数参数。
fn foo(s: &String) {
println!("in fn foo: {s}");
}
fn main() {
let s1 = String::from("I am a superman.");
foo(&s1); // 注意这里传的是字符串的引用 &s1
println!("{s1}"); // 这里可以打印s1的值了
}
// 可以看到,打印出了正确的结果。
// in fn foo: I am a superman.
// I am a superman.
将字符串的可变引用传进函数,并修改字符串内容。
fn foo(s: &mut String) {
s.push_str(" You are batman.");
}
fn main() {
let mut s1 = String::from("I am a superman.");
println!("{s1}");
foo(&mut s1); // 注意这里传的是字符串的可变引用 &mut s1
println!("{s1}");
}
// 输出:
// I am a superman.
// I am a superman. You are batman.
与期望一致。 foo 函数,不再需要费力地把所有权再传回来了。
Rust 的代码 &s1 和 &mut s1 留下了清晰的足迹。
- 函数参数接受的是可变引用或所有权参数,那里面的逻辑一般都会对其引用的资源进行修改。
- 函数参数只接受不可变引用,那里面的逻辑,就一定不会修改被引用的资源。
简单的参数的签名,将函数的意图表示出来。非常利于代码的阅读。
小结
Rust 这头野牛的怪脾气:
在同一时刻,同一个所有权变量的不可变引用和可变引用两者不能同时存在,不可变引用可以同时存在多个。可变引用具有排它性,只能同时存在一个。
借用结束后,原本的所有权变量会重新恢复可读可写的状态。不可变引用可以被任意复制多份,但可变引用不能被复制,只能转移,体现了可变引用具有一定的所有权特征。所有权和引用模型是 Rust 语言编写高可靠和高性能代码的基础,理解这些模型有助于优化程序效率,提高代码质量。
通过探索性的方式尝试遍历不可变引用与可变引用的各种形式和可能的组合,揭开 Rust 中引用的各种性质以及同所有权的关系,总结了多条相关规则。看起来繁琐,理解起来并不难。不要死记硬背那些条条框框,请亲自敲上面的代码示例,编译并运行它,在实践中去理解它们。久而久之 --> 形成思维习惯。
思考题
1、为何在不可变引用存在的情况下(只是读操作),原所有权变量也无法写入?
fn main() {
let mut a: u32 = 10;
let b = &a;
a = 20;
println!("{}", b);
}
2、可变引用复制时,为什么不允许 copy,而是 move?
回答:
1. 不可变引用的作用域跨越了所有权变量的写入过程,意味着同一个作用域同时存在可变引用和不可变引用,编译器为了防止读取错误,不能通过编译。可以把a = 20放到引用之前,即可编译通过。
2. 可变引用如果可以Copy,就违反了可变引用不能同时存在的规则,因此只能Move.
问题1. 所有权型变量被借用时,不能对所有权型变量进行修改。
问题2. 同一时刻,所有权型变量只能有一个可变引用或多个不可变引用。如果复制,则会有多个不可变引用,违反了借用规则。
1. 不可变引用的语义更像“借 值 用一下”,若在不可变引用作用域结束之前,对所有权变量进行写入,那么这个借的“值”,就没有意义了,不确定是否跟当初借的时候是一致的。
2. 如文中所说,可变引用的作用域不能交叉,如果采用 copy,则两份可变引用其实是互不影响的,即可以交叉,就产生矛盾了。
下面代码,为什么变量b之前的mut是必须的,变量c之前不需要:
fn main() {
let mut a1 = 10u32;
let mut b = &mut a1;
*b = 20;
let c = &mut b;
**c = 30; // 多级解引用操作
println!("{c}");
}
作者回复: 因为后面c要对b进行可变借用。这样,语法上就要求b在前面加mut修饰。如果后面再有d对c进行可变借用,那c也需要加mut修饰。