Update(fight-with-compile): unified format 10

pull/509/head
Allan Downey 3 years ago
parent 9c0908ac8c
commit f44ecccf64

@ -1,10 +1,13 @@
# 智能指针引起的重复借用错误
本文将彻底解决一个困扰广大 Rust 用户已久的常见错误: 当智能指针和结构体一起使用时导致的借用错误: ` cannot borrow `mut_s` as mutable because it is also borrowed as immutable`.
相信看过[<<对抗Rust编译检查系列>>](https://course.rs/fight-with-compiler/intro.html)的读者都知道结构体中的不同字段可以独立借用吧?
本文将彻底解决一个困扰广大 Rust 用户已久的常见错误: 当智能指针和结构体一起使用时导致的借用错误: `cannot borrow`mut_s` as mutable because it is also borrowed as immutable`.
相信看过[<<对抗 Rust 编译检查系列>>](https://course.rs/fight-with-compiler/intro.html)的读者都知道结构体中的不同字段可以独立借用吧?
## 结构体中的字段借用
不知道也没关系,我们这里再简单回顾一下:
```rust
struct Test {
a : u32,
@ -26,13 +29,16 @@ impl Test {
因此,虽然我们不能同时对整个结构体进行多次可变借用,但是我们可以分别对结构体中的不同字段进行可变借用,当然,一个字段至多也只能存在一个可变借用,这个最基本的所有权规则还是不能违反的。变量`a`引用结构体字段`a`,变量`b`引用结构体字段`b`,从底层来说,这种方式也不会造成两个可变引用指向了同一块内存。
## RefCell
如果你还不知道 RefCell可以看看[这篇文章](https://course.rs/advance/smart-pointer/cell-refcell.html)当然不看也行简而言之RefCell 能够实现:
- 将借用规则从编译期推迟到运行期,但是并不会饶过借用规则,当不符合时,程序直接`panic`
- 实现内部可变性:简单来说,对一个不可变的值进行可变借用,然后修改内部的值
## 被RefCell包裹的结构体
## 被 RefCell 包裹的结构体
既然了解了结构体的借用规则和`RefCell`, 我们来看一段结合了两者的代码:
```rust
use std::cell::RefCell;
use std::io::Write;
@ -54,6 +60,7 @@ fn write(s: RefCell<S>) {
```
以上代码从`s`中可变借用出结构体`S`,随后又对结构体中的两个字段进行了分别借用,按照之前的规则这段代码应该顺利通过编译:
```console
error[E0502]: cannot borrow `mut_s` as mutable because it is also borrowed as immutable
--> src/main.rs:16:5
@ -69,12 +76,15 @@ error[E0502]: cannot borrow `mut_s` as mutable because it is also borrowed as im
只能说,还好它报错了,否则本篇文章已经可以结束。。。错误很简单,首先对结构体`S`的`data`字段进行了不可变借用,其次又对`writer`字段进行了可变借用,这个符合之前的规则:对结构体不同字段分开借用,为何报错了?
## 深入分析
第一感觉,问题是出在`borrow_mut`方法返回的类型上,先来看看:
```rust
pub fn borrow_mut(&self) -> RefMut<'_, T>
```
可以看出,该方法并没有直接返回我们的结构体,而是一个`RefMut`类型,而要使用该类型,需要经过编译器为我们做一次隐式的`Deref`转换,编译器展开后的代码大概如下:
```rust
use std::cell::RefMut;
use std::ops::{Deref, DerefMut};
@ -91,6 +101,7 @@ fn write(s: RefCell<S>) {
而上面的`&Deref::deref(&mut_s)`和`DerefMut::deref_mut(&mut mut_s)`函数,签名全部使用的是结构体,并不是结构体中的某一个字段,因此对于编译器来说,该结构体明显是被重复借用了!
## 解决方法
因此要解决这个问题,我们得把之前的展开形式中的`Deref::deref`消除掉,这样没有了函数签名,编译器也将不再懒政。
既然两次`Deref::deref`调用都是对智能指针的自动`Deref`,那么可以提前手动的把它`Deref`了,只做一次!
@ -109,7 +120,9 @@ fn write(s: RefCell<S>) {
此后对`tmp`的使用就回归到文章开头的那段代码:分别借用结构体的不同字段,成功通过编译!
#### 展开代码
我们再来模拟编译器对正确的代码进行一次展开:
```rust
use std::cell::RefMut;
use std::ops::DerefMut;
@ -124,19 +137,22 @@ fn write(s: RefCell<S>) {
可以看出,此时对结构体的使用不再有`DerefMut::deref`的身影,我们成功消除了函数边界对编译器的影响!
## 不仅仅是RefCell
## 不仅仅是 RefCell
事实上,除了 RefCell 外,还有不少会导致这种问题的智能指针,当然原理都是互通的,我们这里就不再进行一一深入讲解,只简单列举下:
- `Box`
- `MutexGuard`(来源于Mutex)
- `PeekMut`(来源于BinaryHeap)
- `RwLockWriteGuard`(来源于RwLock)
- `MutexGuard`(来源于 Mutex)
- `PeekMut`(来源于 BinaryHeap)
- `RwLockWriteGuard`(来源于 RwLock)
- `String`
- `Vec`
- `Pin`
## 一个练习
下面再来一个练习巩固一下,强烈建议大家按照文章的思路进行分析和解决:
```rust
use std::rc::Rc;
use std::cell::RefCell;
@ -164,8 +180,10 @@ fn main() {
```
## 总结
当结构体的引用穿越函数边界时,我们要格外小心,因为编译器只会对函数签名进行检查,并不关心内部到底用了结构体的哪个字段,当签名都使用了结构体时,会立即报错。
而智能指针由于隐式解引用`Deref`的存在,导致了两次`Deref`时都让结构体穿越了函数边界`Deref::deref`,结果造成了重复借用的错误。
解决办法就是提前对智能指针进行手动解引用,然后对内部的值进行借用后,再行使用。

@ -1,3 +1,4 @@
# 重复借用
本章讲述如何解决类似`cannot borrow *self as mutable because it is also borrowed as immutable`这种重复借用的错误。

@ -2,9 +2,10 @@
本文将彻底解决一个困扰广大 Rust 用户已久的常见错误:因为在函数内外同时借用一个引用,导致了重复借用错误`cannot borrow *self as mutable because it is also borrowed as immutable`.
> 本文大部分内容节选自[Rust常见陷阱](https://course.rs/pitfalls/index.html)专题,由于借用是新手绕不过去的坎,因此将其提取出来形成一个新的系列
> 本文大部分内容节选自[Rust 常见陷阱](https://course.rs/pitfalls/index.html)专题,由于借用是新手绕不过去的坎,因此将其提取出来形成一个新的系列
## 正确的代码
```rust
struct Test {
a : u32,
@ -26,6 +27,7 @@ impl Test {
答案要从很久很久之前开始(啊哒~~~由于我太啰嗦,被正义群众来了一下,那咱现在开始长话短说,直接进入主题)。
#### 正确代码为何不报错?
虽然从表面来看,`a`和`b`都可变引用了`self`,但是 Rust 的编译器在很多时候都足够聪明,它发现我们其实仅仅引用了同一个结构体中的不同字段,因此完全可以将其的借用权分离开来。
因此,虽然我们不能同时对整个结构体进行可变引用,但是我们可以分别对结构体中的不同字段进行可变引用,当然,一个字段至多也只能存在一个可变引用,这个最基本的所有权规则还是不能违反的。变量`a`引用结构体字段`a`,变量`b`引用结构体字段`b`,从底层来说,这种方式也不会造成两个可变引用指向了同一块内存。
@ -33,6 +35,7 @@ impl Test {
至此,正确代码我们已经挖掘完毕,再来看看重构后的错误代码。
## 重构后的错误代码
```rust
struct Test {
a : u32,
@ -70,6 +73,7 @@ error[E0499]: cannot borrow `*self` as mutable more than once at a time
嗯,最开始提到的错误,它终于出现了。
## 大聪明编译器
为什么?明明之前还是正确的代码,就因为放入函数中就报错了?我们先从一个简单的理解谈起,当然这个理解也是浮于表面的,等会会深入分析真实的原因。
之前讲到 Rust 编译器挺聪明,可以识别到引用到不同的结构体字段,因此不会报错。但是现在这种情况下,编译器又不够聪明了,一旦放入函数中,编译器将无法理解我们对`self`的使用:它仅仅用到了一个字段,而不是整个结构体。
@ -77,9 +81,11 @@ error[E0499]: cannot borrow `*self` as mutable more than once at a time
因此它会简单的认为,这个结构体作为一个整体被可变借用了,产生两个可变引用,一个引用整个结构体,一个引用了结构体字段`b`,这两个引用存在重叠的部分,最终导致编译错误。
## 被冤枉的编译器
在工作生活中,我们无法理解甚至错误的理解一件事,有时是因为层次不够导致的。同样,对于本文来说,也是因为我们对编译器的所知不够,才冤枉了它,还给它起了一个屈辱的“大聪明”外号。
#### 深入分析
> 如果只改变相关函数的实现而不改变它的签名,那么不会影响编译的结果
何为相关函数?当函数`a`调用了函数`b`,那么`b`就是`a`的相关函数。
@ -87,6 +93,7 @@ error[E0499]: cannot borrow `*self` as mutable more than once at a time
上面这句是一条非常重要的编译准则,意思是,对于编译器来说,只要函数签名没有变,那么任何函数实现的修改都不会影响已有的编译结果(前提是函数实现没有错误- , -)。
以前面的代码为例:
```rust
fn increase_a (&mut self) {
self.a += 1;
@ -102,14 +109,16 @@ fn increase(&mut self) {
虽然`increase_a`在函数实现中没有访问`self.b`字段,但是它的签名允许它访问`b`,因此违背了借用规则。事实上,该函数有没有访问`b`不重要,**因为编译器在这里只关心签名,签名存在可能性,那么就立刻报出错误**。
为何会有这种编译器行为,主要有两个原因:
1. 一般来说,我们希望编译器有能力独立的编译每个函数,而无需深入到相关函数的内部实现,因为这样做会带来快得多的编译速度。
2. 如果没有这种保证,那么在实际项目开发中,我们会特别容易遇到各种错误。 假设我们要求编译器不仅仅关注相关函数的签名,还要深入其内部关注实现,那么由于 Rust 严苛的编译规则,当你修改了某个函数内部实现的代码后,可能会引起使用该函数的其它函数的各种错误!对于大型项目来说,这几乎是不可接受的!
然后,我们的借用类型这么简单,编译器有没有可能针对这种场景,在现有的借用规则之外增加特殊规则?答案是否定的,由于 Rust 语言的设计哲学:特殊规则的加入需要慎之又慎,而我们的这种情况其实还蛮好解决的,因此编译器不会为此新增规则。
## 解决办法
在深入分析中,我们提到一条重要的规则,要影响编译行为,就需要更改相关函数的签名,因此可以修改`increase_a`的签名:
```rust
fn increase_a (a :&mut u32) {
*a += 1;
@ -124,7 +133,6 @@ fn increase(&mut self) {
此时,`increase_a`这个相关函数,不再使用`&mut self`作为签名,而是获取了结构体中的字段`a`,此时编译器又可以清晰的知道:函数`increase_a`和变量`b`分别引用了结构体中的不同字段,因此可以编译通过。
当然,除了修改相关函数的签名,你还可以修改调用者的实现:
```rust
@ -136,8 +144,10 @@ fn increase(&mut self) {
在这里,我们不再单独声明变量`b`,而是直接调用`self.b+=1`进行递增,根据借用生命周期[NLL](https://course.rs/advance/lifetime/advance.html#nllnon-lexical-lifetime)的规则,第一个可变借用`self.increase_a()`的生命周期随着方法调用的结束而结束,那么就不会影响`self.b += 1`中的借用。
## CPU模拟例子
## CPU 模拟例子
我们再来看一个例子:
```rust
use std::collections::HashMap;
@ -191,6 +201,8 @@ fn main() {
```
## 总结
知其然知其所以然,要彻底解决借用导致的编译错误,我们就必须深入了解其原理,心中有剑则手中无"贱"。
上面的例子就留给读者朋友自己去解决,相信你以后在遇到这种常见问题时,会更加游刃有余。

@ -1,11 +1,15 @@
# 当闭包碰到特征对象1
# 当闭包碰到特征对象 1
特征对象是一个好东西闭包也是一个好东西但是如果两者你都想要时可能就会火星撞地球boom! 至于这两者为何会勾搭到一起?考虑一个常用场景:使用闭包作为回调函数.
## 学习目标
如何使用闭包作为特征对象,并解决以下错误:`the parameter type ` \`impl Fn(&str) -> Res\` ` may not live long enough`
## 报错的代码
在下面代码中,我们通过闭包实现了一个简单的回调函数(错误代码已经标注)
```rust
pub struct Res<'a> {
value: &'a str,
@ -60,6 +64,7 @@ error[E0310]: the parameter type `impl Fn(&str) -> Res` may not live long enough
```
从第一感觉来说,报错属实不应该,因为我们连引用都没有用,生命周期都不涉及,怎么就报错了?在继续深入之前,先来观察下该闭包是如何被使用的:
```rust
callback: Option<Box<dyn Fn(&str) -> Res>>,
```
@ -67,14 +72,17 @@ callback: Option<Box<dyn Fn(&str) -> Res>>,
众所周知,闭包跟哈姆雷特一样,每一个都有[自己的类型](../../advance/functional-programing/closure.md#闭包作为函数返回值),因此我们无法通过类型标注的方式来声明一个闭包,那么只有一个办法,就是使用特征对象,因此上面代码中,通过`Box<dyn Trait>`的方式把闭包特征封装成一个特征对象。
## 深入挖掘报错原因
事出诡异必有妖,那接下来我们一起去会会这只妖。
#### 特征对象的生命周期
首先编译器报错提示我们闭包活得不够久,那可以大胆推测,正因为使用了闭包作为特征对象,所以才活得不够久。因此首先需要调查下特征对象的生命周期。
首先给出结论:**特征对象隐式的具有`'static`生命周期**。
其实在Rust中`'static`生命周期很常见,例如一个没有引用字段的结构体它其实也是`'static`。当`'static`用于一个类型时,该类型不能包含任何非`'static`引用字段,例如以下结构体:
其实在 Rust 中,`'static`生命周期很常见,例如一个没有引用字段的结构体它其实也是`'static`。当`'static`用于一个类型时,该类型不能包含任何非`'static`引用字段,例如以下结构体:
```rust
struct Foo<'a> {
x : &'a [u8]
@ -85,8 +93,10 @@ struct Foo<'a> {
对于特征对象来说,它没有包含非`'static`的引用,因此它隐式的具有`'static`生命周期, `Box<dyn Trait>`就跟`Box<dyn Trait + 'static>`是等价的。
#### 'static闭包的限制
#### 'static 闭包的限制
其实以上代码的错误很好解决,甚至编译器也提示了我们:
```console
help: consider adding an explicit lifetime bound...: `impl Fn(&str) -> Res + 'static`
```
@ -94,6 +104,7 @@ help: consider adding an explicit lifetime bound...: `impl Fn(&str) -> Res + 'st
但是解决问题不是本文的目标,我们还是要继续深挖一下,如果闭包使用了`'static`会造成什么问题。
##### 1. 无本地变量被捕获
```rust
inl.set(|val| {
println!("Inline: {}", val);
@ -101,9 +112,10 @@ inl.set(|val| {
});
```
以上代码只使用了闭包中传入的参数,并没有本地变量被捕获,因此`'static`闭包一切OK。
以上代码只使用了闭包中传入的参数,并没有本地变量被捕获,因此`'static`闭包一切 OK。
##### 2. 有本地变量被捕获
```rust
let local = "hello".to_string();
@ -117,7 +129,8 @@ inl.set(|val| {
这里我们在闭包中捕获了本地环境变量`local`,因为`local`不是`'static`,那么闭包也不再是`'static`。
##### 3. 将本地变量move进闭包
##### 3. 将本地变量 move 进闭包
```rust
let local = "hello".to_string();
@ -134,7 +147,9 @@ inl.set(move |val| {
如上所示,你也可以选择将本地变量的所有权`move`进闭包中,此时闭包再次具有`'static`生命周期
##### 4. 非要捕获本地变量的引用?
对于第2种情况如果非要这么干那`'static`肯定是没办法了,我们只能给予闭包一个新的生命周期:
对于第 2 种情况,如果非要这么干,那`'static`肯定是没办法了,我们只能给予闭包一个新的生命周期:
```rust
pub struct Container<'a, 'b> {
name: &'a str,
@ -160,7 +175,9 @@ impl<'a, 'b> Container<'a, 'b> {
友情提示:由此修改引发的一系列错误,需要你自行修复: ) (再次友情小提示,可以考虑把`main`中的`local`变量声明位置挪到`inl`声明位置之前)
## 姗姗来迟的正确代码
其实,大家应该都知道该如何修改了,不过出于严谨,我们还是继续给出完整的正确代码:
```rust
pub fn set(&mut self, cb: impl Fn(&str) -> Res + 'static) {
```
@ -168,6 +185,7 @@ pub fn set(&mut self, cb: impl Fn(&str) -> Res + 'static) {
可能大家觉得我重新定义了`完整`两个字,其实是我不想水篇幅:)
## 总结
闭包和特征对象的相爱相杀主要原因就在于特征对象默认具备`'static`的生命周期,同时我们还对什么样的类型具备`'static`进行了简单的分析。
同时,如果一个闭包拥有`'static`生命周期,那闭包无法通过引用的方式来捕获本地环境中的变量。如果你想要非要捕获,只能使用非`'static`。

@ -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};
@ -127,12 +134,14 @@ fn random_empty_tile_2<'arr>(arr: &'arr mut [Tile]) -> &'arr mut Tile {
结果,编译器还是不给通过,报的错误几乎一样
## 深层原因
令人沮丧的是,我找遍了网上,也没有具体的原因,大家都说这是编译器太笨导致的问题,但是关于深层的原因,也没人能说出个所有然。
因此,我无法在本文中给出为什么编译器会这么笨的真实原因,如果以后有结果,会在这里进行更新。
------2022年1月13日更新-------
------2022 1 13 日更新-------
兄弟们,我带着挖掘出的一些内容回来了,再来看段错误代码先:
```rust
struct A {
a: i32
@ -161,7 +170,7 @@ 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`的生命周期等于循环生命周期。
@ -169,6 +178,7 @@ impl A {
但是我们的`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`,但是可能会导致编译速度变慢,或者引入一些新的编译错误。
## 总结
编译器不是万能的,它也会迷茫,也会犯错。
因此我们在循环中使用引用类型时要格外小心,特别是涉及可变引用,这种情况下,最好的办法就是避免中间状态,或者在返回时避免使用中间状态。

@ -1,8 +1,9 @@
# 生命周期声明的范围过大
在大多时候Rust的生命周期你只要标识了即可以通过编译但是总是存在一些情况会导致编译无法通过本文就讲述这样一种情况因为生命周期声明的范围过大导致了编译无法通过希望大家喜欢
在大多时候Rust 的生命周期你只要标识了,即可以通过编译,但是总是存在一些情况,会导致编译无法通过,本文就讲述这样一种情况:因为生命周期声明的范围过大,导致了编译无法通过,希望大家喜欢
## 例子 1
## 例子1
```rust
struct Interface<'a> {
manager: &'a mut Manager<'a>
@ -50,7 +51,9 @@ fn use_list(list: &List) {
println!("{}", list.manager.text);
}
```
运行后报错:
```console
error[E0502]: cannot borrow `list` as immutable because it is also borrowed as mutable // `list`无法被借用,因为已经被可变借用
--> src/main.rs:40:14
@ -70,6 +73,7 @@ error[E0502]: cannot borrow `list` as immutable because it is also borrowed as m
这是因为我们在`get_interface`方法中声明的`lifetime`有问题,该方法的参数的生明周期是`'a`,而`List`的生命周期也是`'a`,说明该方法至少活得跟`List`一样久,再回到`main`函数中,`list`可以活到`main`函数的结束,因此`list.get_interface()`借用的可变引用也会活到`main`函数的结束,在此期间,自然无法再进行借用了。
要解决这个问题,我们需要为`get_interface`方法的参数给予一个不同于`List<'a>`的生命周期`'b`,最终代码如下:
```rust
struct Interface<'b, 'a: 'b> {
manager: &'b mut Manager<'a>
@ -121,6 +125,7 @@ fn use_list(list: &List) {
```
当然,咱还可以给生命周期给予更有意义的名称:
```rust
struct Interface<'text, 'manager> {
manager: &'manager mut Manager<'text>

@ -1,6 +1,7 @@
# 生命周期过大-02
继上篇文章后,我们再来看一段**可能**涉及生命周期过大导致的无法编译问题:
```rust
fn bar(writer: &mut Writer) {
baz(writer.indent());
@ -37,6 +38,7 @@ fn main() {}
```
报错如下:
```console
error[E0623]: lifetime mismatch
--> src/main.rs:2:16
@ -52,6 +54,7 @@ error[E0623]: lifetime mismatch
WTF这什么报错之前都没有见过而且很难理解什么叫`writer`滑入了另一个`writer`
别急,我们先来仔细看下代码,注意这一段:
```rust
impl<'a> Writer<'a> {
fn indent(&'a mut self) -> &'a mut Self {
@ -61,9 +64,11 @@ impl<'a> Writer<'a> {
}
}
```
这里的生命周期定义说明`indent`方法使用的。。。等等!你的代码错了,你怎么能在一个函数中返回一个新创建实例的引用?!!最重要的是,编译器不提示这个错误,竟然提示一个莫名其妙看不懂的东东。
行,那我们先解决这个问题,将该方法修改为:
```rust
fn indent(&'a mut self) -> Writer<'a> {
Writer {
@ -74,6 +79,7 @@ fn indent(&'a mut self) -> Writer<'a> {
```
怀着惴惴这心,再一次运行程序,果不其然,编译器又朝我们扔了一坨错误:
```console
error[E0308]: mismatched types
--> src/main.rs:2:9
@ -86,6 +92,7 @@ error[E0308]: mismatched types
```
哦,这次错误很明显,因为`baz`需要`&mut Writer`,但是咱们`writer.indent`返回了一个`Writer`,因此修改下即可:
```rust
fn bar(writer: &mut Writer) {
baz(&mut writer.indent());
@ -94,6 +101,7 @@ fn bar(writer: &mut Writer) {
```
这次总该成功了吧?再次心慌慌的运行编译器,哐:
```console
error[E0623]: lifetime mismatch
--> src/main.rs:2:21
@ -109,6 +117,7 @@ error[E0623]: lifetime mismatch
可恶,还是这个看不懂的错误,仔细检查了下代码,这次真的没有其他错误了,只能硬着头皮上。
大概的意思可以分析,生命周期范围不匹配,说明一个大一个小,然后一个`writer`中流入到另一个`writer`说明,两个`writer`的生命周期定义错了,既然这里提到了`indent`方法调用,那么我们再去仔细看一眼:
```rust
impl<'a> Writer<'a> {
fn indent(&'a mut self) -> Writer<'a> {
@ -120,11 +129,13 @@ impl<'a> Writer<'a> {
...
}
```
好像有点问题,`indent`返回的`Writer`的生命周期和外面调用者的`Writer`的生命周期一模一样,这很不合理,一眼就能看出前者远小于后者。
这里稍微展开以下,为何`indent`方法返回值的生命周期不能与参数中的`self`相同。**首先,我们假设它们可以相同,也就是上面的代码可以编译通过**,由于此时在返回值中借用了`self`的可变引用,意味着**如果你在返回值被使用后,还继续使用`self` 会导致重复借用的错误,因为返回值的生命周期将持续到 `self` 结束**。
既然不能相同,那我们尝试着修改下`indent`:
```rust
fn indent<'b>(&'b mut self) -> Writer<'b> {
Writer {
@ -135,6 +146,7 @@ impl<'a> Writer<'a> {
```
Bang! 编译成功,不过稍等,回想下生命周期消除的规则,我们还可以实现的更优雅:
```rust
fn bar(writer: &mut Writer) {
baz(&mut writer.indent());
@ -171,3 +183,4 @@ fn main() {}
```
至此,问题彻底解决,太好了,我感觉我又变强了。可是默默看了眼自己的头发,只能以`哎~`一声叹息结束本章内容。

Loading…
Cancel
Save