|
|
|
@ -1,8 +1,8 @@
|
|
|
|
|
# 深入生命周期
|
|
|
|
|
其实关于生命周期的常用特性,在上一节中,我们已经概括的差不多了,本章主要讲解生命周期的一些高级或者不为人知的特性。对于新手,完全可以跳过本节内容,进行下一章节的学习。
|
|
|
|
|
其实关于生命周期的常用特性,在上一节中,我们已经概括得差不多了,本章主要讲解生命周期的一些高级或者不为人知的特性。对于新手,完全可以跳过本节内容,进行下一章节的学习。
|
|
|
|
|
|
|
|
|
|
## 不太聪明的生命周期检查
|
|
|
|
|
在Rust语言学习中,一个很重要的部分就是阅读一些你可能不经常遇到,但是一旦遇到就难以理解的代码,其中这些代码在生命周期中是最常遇到的,这里我们就来看看一些本以为可以编译,但是却因为生命周期系统不够聪明导致编译失败的代码.
|
|
|
|
|
在 Rust 语言学习中,一个很重要的部分就是阅读一些你可能不经常遇到,但是一旦遇到就难以理解的代码,这些代码往往最令人头疼的就是生命周期,这里我们就来看看一些本以为可以编译,但是却因为生命周期系统不够聪明导致编译失败的代码。
|
|
|
|
|
|
|
|
|
|
#### 例子1
|
|
|
|
|
```rust
|
|
|
|
@ -10,7 +10,9 @@
|
|
|
|
|
struct Foo;
|
|
|
|
|
|
|
|
|
|
impl Foo {
|
|
|
|
|
fn mutate_and_share(&mut self) -> &Self { &*self }
|
|
|
|
|
fn mutate_and_share(&mut self) -> &Self {
|
|
|
|
|
&*self
|
|
|
|
|
}
|
|
|
|
|
fn share(&self) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -24,7 +26,7 @@ fn main() {
|
|
|
|
|
|
|
|
|
|
上面的代码中,`foo.mutate_and_share()` 虽然借用了 `&mut self`,但是它最终返回的是一个 `&self`,然后赋值给 `loan`,因此理论上来说它最终是进行了不可变借用,同时 `foo.share` 也进行了不可变借用,那么根据 Rust 的借用规则:多个不可变借用可以同时存在,因此该代码应该编译通过。
|
|
|
|
|
|
|
|
|
|
事实上,运行代码后,你将看到一个错误:
|
|
|
|
|
事实上,运行代码后,你将看到一个错误:
|
|
|
|
|
```console
|
|
|
|
|
error[E0502]: cannot borrow `foo` as immutable because it is also borrowed as mutable
|
|
|
|
|
--> src/main.rs:12:5
|
|
|
|
@ -44,7 +46,9 @@ error[E0502]: cannot borrow `foo` as immutable because it is also borrowed as mu
|
|
|
|
|
struct Foo;
|
|
|
|
|
|
|
|
|
|
impl Foo {
|
|
|
|
|
fn mutate_and_share<'a>(&'a mut self) -> &'a Self { &'a *self }
|
|
|
|
|
fn mutate_and_share<'a>(&'a mut self) -> &'a Self {
|
|
|
|
|
&'a *self
|
|
|
|
|
}
|
|
|
|
|
fn share<'a>(&'a self) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -68,10 +72,10 @@ fn main() {
|
|
|
|
|
|
|
|
|
|
这就解释了可变借用为啥会在 `main` 函数作用域内有效,最终导致 `foo.share()` 无法再进行不可变借用。
|
|
|
|
|
|
|
|
|
|
上述代码实际上完全是正确的,但是因为生命周期系统的"粗糙实现“,导致了编译错误,目前来说,遇到这种生命周期系统不够聪明导致的编译错误,我们也没有太好的办法,只能修改代码去满足它的需求,并期待以后它会更聪明。
|
|
|
|
|
上述代码实际上完全是正确的,但是因为生命周期系统的“粗糙实现”,导致了编译错误,目前来说,遇到这种生命周期系统不够聪明导致的编译错误,我们也没有太好的办法,只能修改代码去满足它的需求,并期待以后它会更聪明。
|
|
|
|
|
|
|
|
|
|
#### 例子2
|
|
|
|
|
再来看一个例子:
|
|
|
|
|
再来看一个例子:
|
|
|
|
|
```rust
|
|
|
|
|
#![allow(unused)]
|
|
|
|
|
fn main() {
|
|
|
|
@ -125,7 +129,7 @@ error[E0499]: cannot borrow `*map` as mutable more than once at a time
|
|
|
|
|
## 无界生命周期
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
不安全代码(`unsafe`)经常会凭空产生引用或生命周期, 这些生命周期被称为是 **无界(unbound)** 的。
|
|
|
|
|
不安全代码(`unsafe`)经常会凭空产生引用或生命周期,这些生命周期被称为是 **无界(unbound)** 的。
|
|
|
|
|
|
|
|
|
|
无界生命周期往往是在解引用一个原生指针(裸指针raw pointer)时产生的,换句话说,它是凭空产生的,因为输入参数根本就没有这个生命周期:
|
|
|
|
|
```rust
|
|
|
|
@ -136,9 +140,9 @@ fn f<'a, T>(x: *const T) -> &'a T {
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
上述代码中,参数`x`是一个裸指针,它并没有任何生命周期,然后通过`unsafe`操作后,它被进行了解引用,变成了一个Rust的标准引用类型,该类型必须要有生命周期,也就是`'a`。
|
|
|
|
|
上述代码中,参数 `x` 是一个裸指针,它并没有任何生命周期,然后通过 `unsafe` 操作后,它被进行了解引用,变成了一个 Rust 的标准引用类型,该类型必须要有生命周期,也就是 `'a`。
|
|
|
|
|
|
|
|
|
|
可以看出`'a`是凭空产生的,因此它是无界生命周期。这种生命周期由于没有受到任何约束,因此它想要多大就多大,这实际上比`'static`要强大。例如`&'static &'a T`是无效类型,但是无界生命周期`&'unbounded &'a T`会被视为`&'a &'a T`从而通过编译检查,因为它可大可小,就像孙猴子的棒子一般。
|
|
|
|
|
可以看出 `'a` 是凭空产生的,因此它是无界生命周期。这种生命周期由于没有受到任何约束,因此它想要多大就多大,这实际上比 `'static` 要强大。例如 `&'static &'a T` 是无效类型,但是无界生命周期 `&'unbounded &'a T` 会被视为 `&'a &'a T` 从而通过编译检查,因为它可大可小,就像孙猴子的金箍棒一般。
|
|
|
|
|
|
|
|
|
|
我们在实际应用中,要尽量避免这种无界生命周期。最简单的避免无界生命周期的方式就是在函数声明中运用生命周期消除规则。**若一个输出生命周期被消除了,那么必定因为有一个输入生命周期与之对应**。
|
|
|
|
|
|
|
|
|
@ -146,7 +150,7 @@ fn f<'a, T>(x: *const T) -> &'a T {
|
|
|
|
|
生命周期约束跟特征约束类似,都是通过形如 `'a: 'b` 的语法,来说明两个生命周期的长短关系。
|
|
|
|
|
|
|
|
|
|
#### 'a: 'b
|
|
|
|
|
假设有两个引用`&'a i32`和`&'b i32`,它们的生命周期分别是`'a`和`'b`,若`'a` >= `'b`,则可以定义`'a:'b`,表示`'a`至少要活得跟`'b`一样久.
|
|
|
|
|
假设有两个引用 `&'a i32` 和 `&'b i32`,它们的生命周期分别是 `'a` 和 `'b`,若 `'a` >= `'b`,则可以定义 `'a:'b`,表示 `'a` 至少要活得跟 `'b` 一样久。
|
|
|
|
|
```rust
|
|
|
|
|
struct DoubleRef<'a,'b:'a, T> {
|
|
|
|
|
r: &'a T,
|
|
|
|
@ -154,10 +158,10 @@ struct DoubleRef<'a,'b:'a, T> {
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
例如上述代码定义一个结构体,它拥有两个引用字段,类型都是泛型`T`, 每个引用都拥有自己的生命周期,由于我们使用了生命周期约束`'b: 'a`,因此`'b`必须活得比`'a`久,也就是结构体中的`r`字段引用的值必须要比`s`字段引用的值活得要久。
|
|
|
|
|
例如上述代码定义一个结构体,它拥有两个引用字段,类型都是泛型 `T`,每个引用都拥有自己的生命周期,由于我们使用了生命周期约束 `'b: 'a`,因此 `'b` 必须活得比 `'a` 久,也就是结构体中的 `r` 字段引用的值必须要比 `s` 字段引用的值活得要久。
|
|
|
|
|
|
|
|
|
|
#### T: 'a
|
|
|
|
|
表示类型`T`必须比`'a`活得要久:
|
|
|
|
|
表示类型 `T` 必须比 `'a` 活得要久:
|
|
|
|
|
```rust
|
|
|
|
|
struct Ref<'a, T: 'a> {
|
|
|
|
|
r: &'a T
|
|
|
|
@ -166,7 +170,7 @@ struct Ref<'a, T: 'a> {
|
|
|
|
|
|
|
|
|
|
因为结构体字段 `r` 引用了 `T`,因此 `r` 的生命周期 `'a` 必须要比 `T` 的生命周期更短(被引用者的生命周期必须要比引用长)。
|
|
|
|
|
|
|
|
|
|
在Rust 1.30版本之前,该写法是必须的,但是从1.31版本开始,编译器可以自动推导`T: 'a`类型的约束,因此我们只需这样写即可:
|
|
|
|
|
在 Rust 1.30 版本之前,该写法是必须的,但是从 1.31 版本开始,编译器可以自动推导 `T: 'a` 类型的约束,因此我们只需这样写即可:
|
|
|
|
|
```rust
|
|
|
|
|
struct Ref<'a, T> {
|
|
|
|
|
r: &'a T
|
|
|
|
@ -187,11 +191,11 @@ impl<'a: 'b, 'b> ImportantExcerpt<'a> {
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
上面的例子中必须添加约束`'a: 'b`后,才能成功编译,不然将`&'a`类型的生命周期强行转换为`&'b`类型,会报错,只有在`'a` >= `'b`的情况下,`'a`才能转换成`'b`.
|
|
|
|
|
上面的例子中必须添加约束 `'a: 'b` 后,才能成功编译,因为 `self.part` 的生命周期与 `self`的生命周期一致,将 `&'a` 类型的生命周期强行转换为 `&'b` 类型,会报错,只有在 `'a` >= `'b` 的情况下,`'a` 才能转换成 `'b`。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 闭包函数的消除规则
|
|
|
|
|
先来看一段简单的代码:
|
|
|
|
|
先来看一段简单的代码:
|
|
|
|
|
```rust
|
|
|
|
|
fn fn_elision(x: &i32) -> &i32 { x }
|
|
|
|
|
let closure_slision = |x: &i32| -> &i32 { x };
|
|
|
|
@ -211,11 +215,11 @@ error: lifetime may not live long enough
|
|
|
|
|
|
|
|
|
|
咦?竟然报错了,明明两个一模一样功能的函数,一个正常编译,一个却报错,错误原因是编译器无法推测返回的引用和传入的引用谁活得更久!
|
|
|
|
|
|
|
|
|
|
真的是非常奇怪的错误,学过上一节的读者应该都记得这样一条生命周期消除规则: **如果函数参数中只有一个引用类型,那该引用的生命周期会被自动分配给所有的返回引用**。我们当前的情况完美符合,`function`函数的顺利编译通过,就充分说明了问题。
|
|
|
|
|
真的是非常奇怪的错误,学过上一节的读者应该都记得这样一条生命周期消除规则:**如果函数参数中只有一个引用类型,那该引用的生命周期会被自动分配给所有的返回引用**。我们当前的情况完美符合, `function` 函数的顺利编译通过,就充分说明了问题。
|
|
|
|
|
|
|
|
|
|
首先给出一个结论:**这个问题,可能很难被解决,建议大家遇到后,还是老老实实用正常的函数,不要秀闭包了**。
|
|
|
|
|
|
|
|
|
|
对于函数的生命周期而言,它的消除规则之所以能生效是因为它的生命周期完全体现在签名的引用类型上,在函数体中无需任何体现:
|
|
|
|
|
对于函数的生命周期而言,它的消除规则之所以能生效是因为它的生命周期完全体现在签名的引用类型上,在函数体中无需任何体现:
|
|
|
|
|
```rust
|
|
|
|
|
fn fn_elision(x: &i32) -> &i32 {..}
|
|
|
|
|
```
|
|
|
|
@ -232,7 +236,7 @@ let closure_slision = |x: &i32| -> &i32 { x };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## NLL (Non-Lexical Lifetime)
|
|
|
|
|
之前我们在[引用与借用](../../basic/ownership/borrowing.md#NLL)那一章其实有讲到过这个概念, 简单来说就是:**引用的生命周期正常来说应该从借用开始一直持续到作用域结束**,但是这种规则会让多引用共存的情况变得更复杂:
|
|
|
|
|
之前我们在[引用与借用](../../basic/ownership/borrowing.md#NLL)那一章其实有讲到过这个概念,简单来说就是:**引用的生命周期正常来说应该从借用开始一直持续到作用域结束**,但是这种规则会让多引用共存的情况变得更复杂:
|
|
|
|
|
```rust
|
|
|
|
|
fn main() {
|
|
|
|
|
let mut s = String::from("hello");
|
|
|
|
@ -246,13 +250,13 @@ fn main() {
|
|
|
|
|
println!("{}", r3);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
按照上述规则,这段代码将会报错,因为`r1`和`r2`的不可变引用将持续到`main`函数结束,而在此范围内,我们又借用了`r3`的可变引用,这违反了借用的规则 : 要么多个不可变借用,要么一个可变借用。
|
|
|
|
|
按照上述规则,这段代码将会报错,因为 `r1` 和 `r2` 的不可变引用将持续到 `main` 函数结束,而在此范围内,我们又借用了 `r3` 的可变引用,这违反了借用的规则:要么多个不可变借用,要么一个可变借用。
|
|
|
|
|
|
|
|
|
|
好在,该规则从1.31版本引入NLL后,就变成了:**引用的生命周期从借用处开始,一直持续到最后一次使用的地方**。
|
|
|
|
|
好在,该规则从 1.31 版本引入 `NLL` 后,就变成了:**引用的生命周期从借用处开始,一直持续到最后一次使用的地方**。
|
|
|
|
|
|
|
|
|
|
按照最新的规则,我们再来分析一下上面的代码。`r1` 和 `r2` 不可变借用在 `println!` 后就不再使用,因此生命周期也随之结束,那么 `r3` 的借用就不再违反借用的规则,皆大欢喜。
|
|
|
|
|
|
|
|
|
|
再来看一段关于NLL的代码解释:
|
|
|
|
|
再来看一段关于 `NLL` 的代码解释:
|
|
|
|
|
```rust
|
|
|
|
|
let mut u = 0i32;
|
|
|
|
|
let mut v = 1i32;
|
|
|
|
@ -272,9 +276,9 @@ use(a); // | |
|
|
|
|
|
*a = 5; // <-----------------+ <--------------------------+
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这段代码一目了然, `a`有三段生命周期:`α`,`β`,`γ`,每一段生命周期都随着当前值的最后一次使用而结束。
|
|
|
|
|
这段代码一目了然,`a` 有三段生命周期:`α`,`β`,`γ`,每一段生命周期都随着当前值的最后一次使用而结束。
|
|
|
|
|
|
|
|
|
|
在实际项目中,`NLL`规则可以大幅减少引用冲突的情况,极大的便利了用户,因此广受欢迎,最终该规则甚至演化成一个独立的项目,未来可能会进一步简化我们的使用, `Polonius` :
|
|
|
|
|
在实际项目中,`NLL` 规则可以大幅减少引用冲突的情况,极大的便利了用户,因此广受欢迎,最终该规则甚至演化成一个独立的项目,未来可能会进一步简化我们的使用,`Polonius`:
|
|
|
|
|
|
|
|
|
|
- [项目地址](https://github.com/rust-lang/polonius)
|
|
|
|
|
- [具体介绍](http://smallcultfollowing.com/babysteps/blog/2018/04/27/an-alias-based-formulation-of-the-borrow-checker/)
|
|
|
|
@ -282,7 +286,7 @@ use(a); // | |
|
|
|
|
|
# Reborrow 再借用
|
|
|
|
|
学完 `NLL` 后,我们就有了一定的基础,可以继续学习关于借用和生命周期的一个高级内容:**再借用**。
|
|
|
|
|
|
|
|
|
|
先来看一段代码:
|
|
|
|
|
先来看一段代码:
|
|
|
|
|
```rust
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
struct Point {
|
|
|
|
@ -310,7 +314,7 @@ fn main() {
|
|
|
|
|
|
|
|
|
|
以上代码,大家可能会觉得可变引用 `r` 和不可变引用 `rr` 同时存在会报错吧?但是事实上并不会,原因在于 `rr` 是对 `r` 的再借用。
|
|
|
|
|
|
|
|
|
|
对于再借用而言,`rr`在借用时不会破坏借用规则,但是你不能在它的生命周期内再使用原来的借用`r`,来看看对上段代码的分析:
|
|
|
|
|
对于再借用而言,`rr` 再借用时不会破坏借用规则,但是你不能在它的生命周期内再使用原来的借用 `r`,来看看对上段代码的分析:
|
|
|
|
|
```rust
|
|
|
|
|
fn main() {
|
|
|
|
|
let mut p = Point { x: 0, y: 0 };
|
|
|
|
@ -334,10 +338,10 @@ fn read_length(strings: &mut Vec<String>) -> usize {
|
|
|
|
|
strings.len()
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
如上所示,函数体内对参数的二次借用也是典型的Reborrow场景。
|
|
|
|
|
如上所示,函数体内对参数的二次借用也是典型的 `Reborrow` 场景。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
那么下面让我们来做件坏事, 破坏这条规则,使其报错:
|
|
|
|
|
那么下面让我们来做件坏事,破坏这条规则,使其报错:
|
|
|
|
|
```rust
|
|
|
|
|
fn main() {
|
|
|
|
|
let mut p = Point { x: 0, y: 0 };
|
|
|
|
@ -363,7 +367,7 @@ impl<'a> Reader for BufReader<'a> {
|
|
|
|
|
// impl内部实际上没有用到'a
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
如果你以前写的`impl`块长上面这样, 同时在`impl`内部的方法中,根本就没有用到`'a`,那就可以写成下面的代码形式。
|
|
|
|
|
如果你以前写的`impl`块长上面这样,同时在 `impl` 内部的方法中,根本就没有用到 `'a`,那就可以写成下面的代码形式。
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
impl Reader for BufReader<'_> {
|
|
|
|
@ -455,7 +459,7 @@ error[E0502]: cannot borrow `list` as immutable because it is also borrowed as m
|
|
|
|
|
| mutable borrow later used here // 可变借用在这里结束
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这段代码看上去并不复杂,实际上难度挺高的,首先在直觉上,`list.get_interface()`借用的可变引用,按理来说应该在这行代码结束后,就归还了,为何能持续到`use_list(&list)`后面呢?
|
|
|
|
|
这段代码看上去并不复杂,实际上难度挺高的,首先在直觉上,`list.get_interface()` 借用的可变引用,按理来说应该在这行代码结束后,就归还了,但是为什么还能持续到 `use_list(&list)` 后面呢?
|
|
|
|
|
|
|
|
|
|
这是因为我们在 `get_interface` 方法中声明的 `lifetime` 有问题,该方法的参数的生命周期是 `'a`,而 `List` 的生命周期也是 `'a`,说明该方法至少活得跟 `List` 一样久,再回到 `main` 函数中,`list` 可以活到 `main` 函数的结束,因此 `list.get_interface()` 借用的可变引用也会活到 `main` 函数的结束,在此期间,自然无法再进行借用了。
|
|
|
|
|
|
|
|
|
@ -509,5 +513,4 @@ fn use_list(list: &List) {
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
至此,生命周期终于完结,两章超级长的内容,可以满足几乎所有对生命周期的学习目标。学完生命周期,意味着你正式入门了Rust,只要再掌握几个常用概念,就可以上手写项目了,下面让我们看看在实际项目中极其常见的功能 - 迭代器.
|
|
|
|
|
至此,生命周期终于完结,两章超级长的内容,可以满足几乎所有对生命周期的学习目标。学完生命周期,意味着你正式入门了 Rust,只要再掌握几个常用概念,就可以上手写项目了,下面让我们看看在实际项目中极其常见的功能 - 迭代器。
|
|
|
|
|