|
|
|
## 高级生命周期
|
|
|
|
|
|
|
|
回顾一下第10章, 我们学习了如何用生命周期注解引用参数来帮助 Rust 理解不同的引用所关联的生命周期. 我们看到大多数时候, Rust 都会让你忽略生命周期, 但是每个引用都有一个生命周期. 还有三个关于生命周期的高级特性我们以前没有介绍, 它们是: *生命周期子类型(lifetime subtyping)*, *生命周期绑定(lifetime
|
|
|
|
bounds)*, 和 *trait 对象生命周期*.
|
|
|
|
|
|
|
|
### 生命周期子类型
|
|
|
|
|
|
|
|
想象一下我们想写一个解释器. 为此, 我们需要一个持有即将被解析的字符串的引用的结构, 我们把这个结构叫做`Context`. 我们将写一个能够解析这个字符串并返回成功或失败的解析器. 该解析器需要借用这个上下文(解析器中的`context`属性)来完成解析. 实现这个功能的代码如例 19-12, 但是这个代码不能被编译因为我们没有使用生命周期注解:
|
|
|
|
|
|
|
|
```rust,ignore
|
|
|
|
struct Context(&str);
|
|
|
|
|
|
|
|
struct Parser {
|
|
|
|
context: &Context,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Parser {
|
|
|
|
fn parse(&self) -> Result<(), &str> {
|
|
|
|
Err(&self.context.0[1..])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
<span class="caption">例19-12: 定义持有一个字符串切片的`Context`结构, 一个持有某个`Context`实例引用的`Parser`结构, 和一个总是返回一个错误的`parse`方法, 这个被返回的错误引用了该字符串切片</span>
|
|
|
|
|
|
|
|
为了简单起见, 我们的`parse`函数返回一个`Result<(), &str>`. 也就是说, 我们在成功时不做任何事情, 在失败时我们返回部分没有解析正确的字符串切片. 一个真正的实现将会有更多的错误信息, 而且实际上在解析成功时会返回当时创建的内容, 但是我们将实现的这部分省略了因为它们与本例的生命周期无关. 我们也定义`parse`总在第一个字节后产生一个错误. 请注意如果第一个字节不在有效的字符边界内这可能会出现错误; 再说一下, 为了把注意力放在生命周期上, 我们简化了这个例子.
|
|
|
|
|
|
|
|
那么我们如何设置`Context`中的字符串切片的生命周期参数和`Parser`中的`Context`引用呢? 最直接的办法就是使用同样的生命周期, 如例19-13所示:
|
|
|
|
|
|
|
|
```rust
|
|
|
|
struct Context<'a>(&'a str);
|
|
|
|
|
|
|
|
struct Parser<'a> {
|
|
|
|
context: &'a Context<'a>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Parser<'a> {
|
|
|
|
fn parse(&self) -> Result<(), &str> {
|
|
|
|
Err(&self.context.0[1..])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
<span class="caption">例19-13: 限定`Context`和`Parser`中的所有引用具有同样的生命周期参数</span>
|
|
|
|
|
|
|
|
这样就能够编译了. 然后, 在例19-14中, 让我们写一个以`Context`实例为参数的函数, 该函数用一个`Parser`来解析那个`Context`实例并把`parse`方法的结果直接返回. 但是这个代码不能正常工作:
|
|
|
|
|
|
|
|
```rust,ignore
|
|
|
|
fn parse_context(context: Context) -> Result<(), &str> {
|
|
|
|
Parser { context: &context }.parse()
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
<span class="caption">例19-14: 尝试添加有一个`parse_context`函数, 该函数有一个`Context`参数, 在函数中使用了`Parser`</span>
|
|
|
|
|
|
|
|
当我们试图用新添加的`parse_context`函数来编译代码时我们会得到两个相当详细的错误:
|
|
|
|
|
|
|
|
```text
|
|
|
|
error: borrowed value does not live long enough
|
|
|
|
--> <anon>:16:5
|
|
|
|
|
|
|
|
|
16 | Parser { context: &context }.parse()
|
|
|
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ does not live long enough
|
|
|
|
17 | }
|
|
|
|
| - temporary value only lives until here
|
|
|
|
|
|
|
|
|
note: borrowed value must be valid for the anonymous lifetime #1 defined on the
|
|
|
|
body at 15:55...
|
|
|
|
--> <anon>:15:56
|
|
|
|
|
|
|
|
|
15 | fn parse_context(context: Context) -> Result<(), &str> {
|
|
|
|
| ________________________________________________________^
|
|
|
|
16 | | Parser { context: &context }.parse()
|
|
|
|
17 | | }
|
|
|
|
| |_^
|
|
|
|
|
|
|
|
error: `context` does not live long enough
|
|
|
|
--> <anon>:16:24
|
|
|
|
|
|
|
|
|
16 | Parser { context: &context }.parse()
|
|
|
|
| ^^^^^^^ does not live long enough
|
|
|
|
17 | }
|
|
|
|
| - borrowed value only lives until here
|
|
|
|
|
|
|
|
|
note: borrowed value must be valid for the anonymous lifetime #1 defined on the
|
|
|
|
body at 15:55...
|
|
|
|
--> <anon>:15:56
|
|
|
|
|
|
|
|
|
15 | fn parse_context(context: Context) -> Result<(), &str> {
|
|
|
|
| ________________________________________________________^
|
|
|
|
16 | | Parser { context: &context }.parse()
|
|
|
|
17 | | }
|
|
|
|
| |_^
|
|
|
|
```
|
|
|
|
|
|
|
|
这些错误表明不管是我们创建的`Parser`实例还是作用于从`Parser`被创建的行开始到`parse_context`函数结束的`context`参数, 都需要拥有整个函数的生命周期.
|
|
|
|
|
|
|
|
换句话说, `Parser`和`context`存活的时间要比整个函数更长, 为了让代码中的所有引用都有效它们也应该在函数被调用前后都有效. 不管是我们正创建的`Parser`还是在函数结束时就会结束作用域的`context`参数都是如此(因为`parse_context`函数会获得`context`的所有权).
|
|
|
|
|
|
|
|
让我们再看一下例19-13中的定义, 特别是`parse`方法的声明:
|
|
|
|
|
|
|
|
```rust,ignore
|
|
|
|
fn parse(&self) -> Result<(), &str> {
|
|
|
|
```
|
|
|
|
|
|
|
|
还记得生命周期的省略规则吗? 如果我们注解引用的生命周期, 那么可以这样写:
|
|
|
|
|
|
|
|
```rust,ignore
|
|
|
|
fn parse<'a>(&'a self) -> Result<(), &'a str> {
|
|
|
|
```
|
|
|
|
|
|
|
|
也就是说, `parse`函数返回值的错误中的部分是因为它有一个生命周期被绑定到了`Parser`实例的生命周期上面(也就是`parse`方法中的`&self`). 这就是了, 因为被返回的字符串切片引用了`Parser`持有的`Context`实例中的字符串切片, 并且我们已经在`Parser`结构的定义中指定了`Parser`中的`Context`的引用的生命周期和`Context`持有的字符串切片的生命周期是一样的.
|
|
|
|
|
|
|
|
问题是`parse_context`函数要返回`parse`方法的返回值, 这样`parse_context`的返回值的生命周期也就被绑定到了`Parser`的生命周期上了. 但是在`parse_context`函数中创建的`Parser`实例在函数结束后就不会存活了(它是临时的), 并且`context`在函数结束后也会越过作用域(`parse_context`拥有它的所有权).
|
|
|
|
|
|
|
|
我们不能返回只在函数作用域内才能存活的值的引用. Rust就认为我们在做这个事情, 因为我们把所有的生命周期参数都注解成一样的了. 这就告诉 Rust `Context`持有的字符串切片的生命周期和`Parser`持有的`Context`引用的生命周期是一样的.
|
|
|
|
|
|
|
|
`parse_context`函数看不到`parse`函数中的东西, 被返回的字符串切片的存活时间将比`Context`和`Parser`更长, 并且`parse_context`返回的是字符串切片的引用而不是`Context`和`Parser`的引用.
|
|
|
|
|
|
|
|
通过了解`parse`的实现, 我们明白了`parse`的返回值被绑定到`Parser`的唯一原因是因为它引用了`Prser`中的`Context`, 也就是对一个字符串切片的引用, 所以它的生命周期才是`parse_context`需要关心的字符串切片的真正的生命周期. 我们需要告诉 Rust 在`Context`中的字符串切片和对`Parser`中的`Context`的引用有不同的生命周期, 我们还要告诉 Rust `parse_context` 函数的返回值被绑定到了`Context`中的字符串切片的生命周期.
|
|
|
|
|
|
|
|
我们可以尝试像例 19-15 中显示的那样只给`Parser`和`Context`不同的生命周期参数. 我们选择了生命周期参数`'s`和`'c`, 这样可以很方便的区分哪个生命周期伴随`Context`中的字符串切片, 哪个生命周期伴随`Parser`中的`Context`引用. 注意这还不能完全修正问题, 但这是一个开始, 编译时我们就会明白为什么它还不够.
|
|
|
|
|
|
|
|
```rust,ignore
|
|
|
|
struct Context<'s>(&'s str);
|
|
|
|
|
|
|
|
struct Parser<'c, 's> {
|
|
|
|
context: &'c Context<'s>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'c, 's> Parser<'c, 's> {
|
|
|
|
fn parse(&self) -> Result<(), &'s str> {
|
|
|
|
Err(&self.context.0[1..])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_context(context: Context) -> Result<(), &str> {
|
|
|
|
Parser { context: &context }.parse()
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
<span class="caption">例19-15: 给对字符串切片和对`Context`的引用指定不同的生命周期参数</span>
|
|
|
|
|
|
|
|
在例 19-13 中, 我们在所有相同的地方注释了引用的生命周期, 但是是否使用不同的参数却依赖于引用是否与字符串切片或`Context`一起使用. 我们也在`parse`的返回值的字符串切片部分添加了一个生命周期注解来表明它与`Context`的生命周期一样.
|
|
|
|
|
|
|
|
下面就是例19-15编译的结果:
|
|
|
|
|
|
|
|
```text
|
|
|
|
error[E0491]: in type `&'c Context<'s>`, reference has a longer lifetime than the data it references
|
|
|
|
--> src/main.rs:4:5
|
|
|
|
|
|
|
|
|
4 | context: &'c Context<'s>,
|
|
|
|
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
|
|
|
|
|
note: the pointer is valid for the lifetime 'c as defined on the struct at 3:0
|
|
|
|
--> src/main.rs:3:1
|
|
|
|
|
|
|
|
|
3 | / struct Parser<'c, 's> {
|
|
|
|
4 | | context: &'c Context<'s>,
|
|
|
|
5 | | }
|
|
|
|
| |_^
|
|
|
|
note: but the referenced data is only valid for the lifetime 's as defined on the struct at 3:0
|
|
|
|
--> src/main.rs:3:1
|
|
|
|
|
|
|
|
|
3 | / struct Parser<'c, 's> {
|
|
|
|
4 | | context: &'c Context<'s>,
|
|
|
|
5 | | }
|
|
|
|
| |_^
|
|
|
|
```
|
|
|
|
|
|
|
|
Rust 并不知道 `'c` 和 `'s` 之间的关系. `Context`中被引用的数据的生命周期是`'s`, 对`Context`的引用有生命周期`'c`, 为了让代码有效, 需要保证生命周期 `'s` 比生命周期 `'c` 更长. 如果 `'s` 存活的时间没有 `'c` 长, 那么对 `Context` 的引用可能就要出问题.
|
|
|
|
|
|
|
|
本节就是为了解决这个问题: Rust 有一个被称为 *生命周期子类型 (lifetime subtyping)* 的特性, 它可以指明一个生命周期参数的存活时间不会比另一个生命周期短. 在我们声明生命周期的尖括号里, 我们可以声明一个普通的生命周期 `'a`, 还可以通过语法 `'b: 'a` 声明一个存活时间至少和 `'a` 一样长的生命周期 `'b`.
|
|
|
|
|
|
|
|
在我们的 `Parser` 的定义里, 为了保证 `'s`(字符串切片的生命周期) 的生命周期的存活时间至少和 `'c`(对 `Context` 引用的生命周期) 一样长, 我们把生命周期的声明改成这样:
|
|
|
|
|
|
|
|
```rust
|
|
|
|
# struct Context<'a>(&'a str);
|
|
|
|
#
|
|
|
|
struct Parser<'c, 's: 'c> {
|
|
|
|
context: &'c Context<'s>,
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
现在, 对`Parser`中的`Context`的引用和对`Context`中的字符串切片的引用就有了不同的生命周期, 并且我们还保证对字符串切片的引用的生命周期比对`Context`的引用的生命周期更长.
|
|
|
|
|
|
|
|
哦, 这个例子真的很长, 但正如本章开头所说, 这些功能非常适用. 你不会经常使用这个语法, 但是在你需要引用另一个引用中的某些内容的时候你就用得上它了.
|
|
|
|
|
|
|
|
### 生命周期绑定
|
|
|
|
|
|
|
|
我们在第10章里面讨论过如何在泛型上使用 trait 绑定. 我们也可以在泛型上添加生命周期参数来对它进行约束. 比如, 我们想在引用上做一个封装. 还记得第15章中的 `RefCell<T>` 吗? 它就是 `borrow` 和 `borrow_mut` 方法的工作原理; 为了在运行时追踪借用规则它们返回引用的封装. 例 19-16 中给出了一个没有生命周期参数的结构的定义:
|
|
|
|
|
|
|
|
```rust,ignore
|
|
|
|
struct Ref<T>(&T);
|
|
|
|
```
|
|
|
|
|
|
|
|
<span class="caption">例 19-16: 先不使用生命周期参数定义一个结构来封装一个对泛型的引用</span>
|
|
|
|
|
|
|
|
但是, 不使用生命周期会产生编译错误, 因为 Rust 并不不知道泛型类型 `T` 会存活多久:
|
|
|
|
|
|
|
|
```text
|
|
|
|
error[E0309]: the parameter type `T` may not live long enough
|
|
|
|
--> <anon>:2:19
|
|
|
|
|
|
|
|
|
2 | struct Ref<'a, T>(&'a T);
|
|
|
|
| ^^^^^^
|
|
|
|
|
|
|
|
|
= help: consider adding an explicit lifetime bound `T: 'a`...
|
|
|
|
note: ...so that the reference type `&'a T` does not outlive the data it points at
|
|
|
|
--> <anon>:2:19
|
|
|
|
|
|
|
|
|
2 | struct Ref<'a, T>(&'a T);
|
|
|
|
| ^^^^^^
|
|
|
|
```
|
|
|
|
|
|
|
|
如果我们把 `T` 换成一个具体的类型我们也会得到同样的错误, 比如像 `struct Ref(&i32)`; 在结构定义中的所有引用都需要一个生命周期参数. 然而, 因为我们有一个泛型类型参数, 所以我们不能以同样的方式来添加一个生命周期参数. 把 `Ref` 定义成 `struct Ref<'a>(&'a T)` 将会产生一个错误因为 Rust 不知道 `T` 能存活多久. 因为 `T` 可以是任意类型, `T` 本身可以是一个引用, 它也可以是一个持有一个多个引用的类型, 这些被持有的每一个引用都有它们自己的生命周期.
|
|
|
|
|
|
|
|
Rust 帮忙给了我们很好的建议, 它告诉我们在这种情况下如何使用生命周期参数:
|
|
|
|
|
|
|
|
```text
|
|
|
|
consider adding an explicit lifetime bound `T: 'a` so that the reference type
|
|
|
|
`&'a T` does not outlive the data it points to.
|
|
|
|
```
|
|
|
|
|
|
|
|
例 19-17 中的代码可以运行因为语法 `T: 'a` 指明 `T` 可以是任意类型, 但是如果它包含任意引用, `T` 必须存活至少有 `'a` 那么长:
|
|
|
|
|
|
|
|
```rust
|
|
|
|
struct Ref<'a, T: 'a>(&'a T);
|
|
|
|
```
|
|
|
|
|
|
|
|
<span class="caption">例 19-17: Adding lifetime bounds on `T` to specify
|
|
|
|
that any references in `T` live at least as long as `'a`</span>
|
|
|
|
|
|
|
|
We could choose to solve this in a different way as shown in Listing 19-18 by
|
|
|
|
bounding `T` on `'static`. This means if `T` contains any references, they must
|
|
|
|
have the `'static` lifetime:
|
|
|
|
|
|
|
|
```rust
|
|
|
|
struct StaticRef<T: 'static>(&'static T);
|
|
|
|
```
|
|
|
|
|
|
|
|
<span class="caption">Listing 19-18: Adding a `'static` lifetime bound to `T`
|
|
|
|
to constrain `T` to types that have only `'static` references or no
|
|
|
|
references</span>
|
|
|
|
|
|
|
|
Types with no references count as `T: 'static`. Because `'static` means the
|
|
|
|
reference must live as long as the entire program, a type that contains no
|
|
|
|
references meets the criteria of all references living as long as the entire
|
|
|
|
program (since there are no references). Think of it this way: if the borrow
|
|
|
|
checker is concerned about references living long enough, then there's no real
|
|
|
|
distinction between a type that has no references and a type that has
|
|
|
|
references that live forever; both of them are the same for the purpose of
|
|
|
|
determining whether or not a reference has a shorter lifetime than what it
|
|
|
|
refers to.
|
|
|
|
|
|
|
|
### Trait Object Lifetimes
|
|
|
|
|
|
|
|
In Chapter 17, we learned about trait objects that consist of putting a trait
|
|
|
|
behind a reference in order to use dynamic dispatch. However, we didn't discuss
|
|
|
|
what happens if the type implementing the trait used in the trait object has a
|
|
|
|
lifetime. Consider Listing 19-19, where we have a trait `Foo` and a struct
|
|
|
|
`Bar` that holds a reference (and thus has a lifetime parameter) that
|
|
|
|
implements trait `Foo`, and we want to use an instance of `Bar` as the trait
|
|
|
|
object `Box<Foo>`:
|
|
|
|
|
|
|
|
```rust
|
|
|
|
trait Foo { }
|
|
|
|
|
|
|
|
struct Bar<'a> {
|
|
|
|
x: &'a i32,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Foo for Bar<'a> { }
|
|
|
|
|
|
|
|
let num = 5;
|
|
|
|
|
|
|
|
let obj = Box::new(Bar { x: &num }) as Box<Foo>;
|
|
|
|
```
|
|
|
|
|
|
|
|
<span class="caption">Listing 19-19: Using a type that has a lifetime parameter
|
|
|
|
with a trait object</span>
|
|
|
|
|
|
|
|
This code compiles without any errors, even though we haven't said anything
|
|
|
|
about the lifetimes involved in `obj`. This works because there are rules
|
|
|
|
having to do with lifetimes and trait objects:
|
|
|
|
|
|
|
|
* The default lifetime of a trait object is `'static`.
|
|
|
|
* If we have `&'a X` or `&'a mut X`, then the default is `'a`.
|
|
|
|
* If we have a single `T: 'a` clause, then the default is `'a`.
|
|
|
|
* If we have multiple `T: 'a`-like clauses, then there is no default; we must
|
|
|
|
be explicit.
|
|
|
|
|
|
|
|
When we must be explicit, we can add a lifetime bound on a trait object like
|
|
|
|
`Box<Foo>` with the syntax `Box<Foo + 'a>` or `Box<Foo + 'static>`, depending
|
|
|
|
on what's needed. Just as with the other bounds, this means that any
|
|
|
|
implementer of the `Foo` trait that has any references inside must have the
|
|
|
|
lifetime specified in the trait object bounds as those references.
|
|
|
|
|
|
|
|
Next, let's take a look at some other advanced features dealing with traits!
|