|
|
|
@ -1,9 +1,11 @@
|
|
|
|
|
# 蠢笨编译器之循环生命周期
|
|
|
|
|
|
|
|
|
|
当涉及生命周期时,Rust编译器有时会变得不太聪明,如果再配合循环,蠢笨都不足以形容它,不信?那继续跟着我一起看看。
|
|
|
|
|
当涉及生命周期时,Rust 编译器有时会变得不太聪明,如果再配合循环,蠢笨都不足以形容它,不信?那继续跟着我一起看看。
|
|
|
|
|
|
|
|
|
|
## 循环中的生命周期错误
|
|
|
|
|
|
|
|
|
|
Talk is cheap, 一起来看个例子:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
use rand::{thread_rng, Rng};
|
|
|
|
|
|
|
|
|
@ -31,6 +33,7 @@ fn random_empty_tile(arr: &mut [Tile]) -> &mut Tile {
|
|
|
|
|
根据以上的分析,可以得出个初步结论:在同一次循环间各个引用生命周期互不影响,在两次循环间,引用也互不影响。
|
|
|
|
|
|
|
|
|
|
那就简单了,开心运行,开心。。。报错:
|
|
|
|
|
|
|
|
|
|
```console
|
|
|
|
|
error[E0502]: cannot borrow `*arr` as immutable because it is also borrowed as mutable
|
|
|
|
|
--> src/main.rs:10:43
|
|
|
|
@ -68,7 +71,9 @@ error[E0499]: cannot borrow `arr[_]` as mutable more than once at a time
|
|
|
|
|
奇了怪了,跟我们之前的分析完全背道而驰,按理来说`arr.len()`的借用应该在调用后立刻结束,而不是持续到后面的代码行;同时可变借用`&mut arr[i]`也应该随着每次循环的结束而结束,为什么会前后两次循环会因为同一处的引用而报错?
|
|
|
|
|
|
|
|
|
|
## 尝试去掉中间变量
|
|
|
|
|
|
|
|
|
|
虽然报错复杂,不过可以看出,所有的错误都跟`tile`这个中间变量有关,我们试着移除它看看:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
use rand::{thread_rng, Rng};
|
|
|
|
|
|
|
|
|
@ -90,7 +95,9 @@ fn random_empty_tile(arr: &mut [Tile]) -> &mut Tile {
|
|
|
|
|
见证奇迹的时刻,竟然编译通过了!到底发什么了什么?仅仅移除了中间变量,就编译通过了?是否可以大胆的猜测,因为中间变量,导致编译器变蠢了,因此无法正确的识别引用的生命周期。
|
|
|
|
|
|
|
|
|
|
## 循环展开
|
|
|
|
|
|
|
|
|
|
如果不使用循环呢?会不会也有这样的错误?咱们试着把循环展开:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
use rand::{thread_rng, Rng};
|
|
|
|
|
|
|
|
|
@ -101,7 +108,7 @@ enum Tile {
|
|
|
|
|
|
|
|
|
|
fn random_empty_tile_2<'arr>(arr: &'arr mut [Tile]) -> &'arr mut Tile {
|
|
|
|
|
let len = arr.len();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// First loop iteration
|
|
|
|
|
{
|
|
|
|
|
let i = thread_rng().gen_range(0..len);
|
|
|
|
@ -109,8 +116,8 @@ fn random_empty_tile_2<'arr>(arr: &'arr mut [Tile]) -> &'arr mut Tile {
|
|
|
|
|
if Tile::Empty == *tile {
|
|
|
|
|
return tile;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Second loop iteration
|
|
|
|
|
{
|
|
|
|
|
let i = thread_rng().gen_range(0..len);
|
|
|
|
@ -121,18 +128,20 @@ fn random_empty_tile_2<'arr>(arr: &'arr mut [Tile]) -> &'arr mut Tile {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
unreachable!()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
结果,编译器还是不给通过,报的错误几乎一样
|
|
|
|
|
|
|
|
|
|
## 深层原因
|
|
|
|
|
|
|
|
|
|
令人沮丧的是,我找遍了网上,也没有具体的原因,大家都说这是编译器太笨导致的问题,但是关于深层的原因,也没人能说出个所有然。
|
|
|
|
|
|
|
|
|
|
因此,我无法在本文中给出为什么编译器会这么笨的真实原因,如果以后有结果,会在这里进行更新。
|
|
|
|
|
|
|
|
|
|
------2022年1月13日更新-------
|
|
|
|
|
------2022 年 1 月 13 日更新-------
|
|
|
|
|
兄弟们,我带着挖掘出的一些内容回来了,再来看段错误代码先:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
struct A {
|
|
|
|
|
a: i32
|
|
|
|
@ -161,14 +170,15 @@ impl A {
|
|
|
|
|
|
|
|
|
|
1. 首先为`two`方法增加一下生命周期标识: `fn two<'a>(&'a mut self) -> &'a i32 { .. }`, 这里根据生命周期的[消除规则](../../advance/lifetime/basic.md#三条消除规则)添加的
|
|
|
|
|
2. 根据生命周期标识可知:`two`中返回的`k`的生命周期必须是`'a`
|
|
|
|
|
3. 根据第2条,又可知:`let k = self.one();`中对`self`的借用生命周期也是`'a`
|
|
|
|
|
3. 根据第 2 条,又可知:`let k = self.one();`中对`self`的借用生命周期也是`'a`
|
|
|
|
|
4. 因为`k`的借用发生在`loop`循环内,因此它需要小于等于循环的生命周期,但是根据之前的推断,它又要大于等于函数的生命周期`'a`,而函数的生命周期又大于等于循环生命周期,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
由上可以推出:`let k = self.one();`中`k`的生命周期要大于等于循环的生命周期,又要小于等于循环的生命周期, 唯一满足条件的就是:`k`的生命周期等于循环生命周期。
|
|
|
|
|
|
|
|
|
|
但是我们的`two`方法在循环中对`k`进行了提前返回,编译器自然会认为存在其它代码,这会导致`k`的生命周期小于循环的生命周期。
|
|
|
|
|
|
|
|
|
|
怎么办呢?很简单:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
fn two(&mut self) -> &i32 {
|
|
|
|
|
loop {
|
|
|
|
@ -183,9 +193,11 @@ fn two(&mut self) -> &i32 {
|
|
|
|
|
> 如果一个引用值从函数的某个路径提前返回了,那么该借用必须要在函数的所有返回路径都合法
|
|
|
|
|
|
|
|
|
|
## 解决方法
|
|
|
|
|
|
|
|
|
|
虽然不能给出原因,但是我们可以看看解决办法,在上面,**移除中间变量**和**消除代码分支**都是可行的方法,还有一种方法就是将部分引用移到循环外面.
|
|
|
|
|
|
|
|
|
|
#### 引用外移
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
fn random_empty_tile(arr: &mut [Tile]) -> &mut Tile {
|
|
|
|
|
let len = arr.len();
|
|
|
|
@ -201,11 +213,13 @@ fn random_empty_tile(arr: &mut [Tile]) -> &mut Tile {
|
|
|
|
|
&mut arr[the_chosen_i]
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
在上面代码中,我们只在循环中保留一个可变引用,剩下的`arr.len`和返回值引用,都移到循环外面,顺利通过编译.
|
|
|
|
|
|
|
|
|
|
在上面代码中,我们只在循环中保留一个可变引用,剩下的`arr.len`和返回值引用,都移到循环外面,顺利通过编译.
|
|
|
|
|
|
|
|
|
|
## 一个更复杂的例子
|
|
|
|
|
|
|
|
|
|
再来看一个例子,代码会更复杂,但是原因几乎相同:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
use std::collections::HashMap;
|
|
|
|
|
|
|
|
|
@ -242,6 +256,7 @@ impl SymbolTable {
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
运行后报错如下:
|
|
|
|
|
|
|
|
|
|
```console
|
|
|
|
|
error[E0499]: cannot borrow `self.scopes` as mutable more than once at a time
|
|
|
|
|
--> src/main.rs:22:25
|
|
|
|
@ -257,6 +272,7 @@ error[E0499]: cannot borrow `self.scopes` as mutable more than once at a time
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
对于上述代码,只需要将返回值修改下,即可通过编译:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
fn get_mut(&mut self, name: &String) -> &mut Symbol {
|
|
|
|
|
let mut current = Some(self.current);
|
|
|
|
@ -276,12 +292,12 @@ fn get_mut(&mut self, name: &String) -> &mut Symbol {
|
|
|
|
|
|
|
|
|
|
其中的关键就在于返回的时候,新建一个引用,而不是使用中间状态的引用。
|
|
|
|
|
|
|
|
|
|
## 新编译器Polonius
|
|
|
|
|
针对现有编译器存在的各种问题,Rust团队正在研发一个全新的编译器,名曰[`polonius`](https://github.com/rust-lang/polonius),但是目前它仍然处在开发阶段,如果想在自己项目中使用,需要在`rustc/RUSTFLAGS`中增加标志`-Zpolonius`,但是可能会导致编译速度变慢,或者引入一些新的编译错误。
|
|
|
|
|
## 新编译器 Polonius
|
|
|
|
|
|
|
|
|
|
针对现有编译器存在的各种问题,Rust 团队正在研发一个全新的编译器,名曰[`polonius`](https://github.com/rust-lang/polonius),但是目前它仍然处在开发阶段,如果想在自己项目中使用,需要在`rustc/RUSTFLAGS`中增加标志`-Zpolonius`,但是可能会导致编译速度变慢,或者引入一些新的编译错误。
|
|
|
|
|
|
|
|
|
|
## 总结
|
|
|
|
|
|
|
|
|
|
编译器不是万能的,它也会迷茫,也会犯错。
|
|
|
|
|
|
|
|
|
|
因此我们在循环中使用引用类型时要格外小心,特别是涉及可变引用,这种情况下,最好的办法就是避免中间状态,或者在返回时避免使用中间状态。
|
|
|
|
|
|
|
|
|
|