Rust 语言从入门到实战 唐刚
基础篇 (11讲)
上节课:
上一节课最后一个例子。想在函数 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 符号,如 &mut 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 修饰符,忘加了,Rust 编译器给指出来了。
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 的心情。
为什么?
又设计了另外一个例子。
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
// 可变借用在这里使用了
怎么回事呢?试着改一下打印语句。
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 中三条关于引用的知识点。
用 引用,改进前面将字符串所有权传进函数,然后又传出来的例子。
第一个例子是将字符串的不可变引用传进函数参数。
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 中引用的各种性质以及同所有权的关系,并总结出了多条相关规则。看起来略显繁琐,但每个示例其实非常简单,理解起来并不困难。不要死记硬背那些条条框框,请你亲自敲上面的代码示例,编译并运行它,在实践中去理解它们。久而久之,就会形成一种思维习惯,觉得 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修饰的。