让我们来看一下这个的简单实现:
struct Parent {
count: u32,
}
struct Child<'a> {
parent: &'a Parent,
}
struct Combined<'a> {
parent: Parent,
child: Child<'a>,
}
impl<'a> Combined<'a> {
fn new() -> Self {
let parent = Parent { count: 42 };
let child = Child { parent: &parent };
Combined { parent, child }
}
}
fn main() {}
这将以错误失败:
error[E0515]: cannot return value referencing local variable `parent`
--> src/main.rs:19:9
|
17 | let child = Child { parent: &parent };
| ------- `parent` is borrowed here
18 |
19 | Combined { parent, child }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ returns a value referencing data owned by the current function
error[E0505]: cannot move out of `parent` because it is borrowed
--> src/main.rs:19:20
|
14 | impl<'a> Combined<'a> {
| -- lifetime `'a` defined here
...
17 | let child = Child { parent: &parent };
| ------- borrow of `parent` occurs here
18 |
19 | Combined { parent, child }
| -----------^^^^^^---------
| | |
| | move out of `parent` occurs here
| returning this value requires that `parent` is borrowed for `'a`
要完全理解这个错误,你必须考虑到值在内存中是如何表示的,以及当你“移动”这些值时会发生什么。让我们用一些假设的内存地址来注释`Combined::new`,以显示值的位置。
let parent = Parent { count: 42 };
let child = Child { parent: &parent };
Combined { parent, child }
孩子应该发生什么?如果像父母一样只是移动了值,那么它将引用不再保证具有有效值的内存。任何其他代码都可以将值存储在内存地址0x1000处。假设该内存是一个整数并访问它可能导致崩溃和/或安全漏洞,这是Rust防止的主要错误类别之一。
这正是“生命周期”所防止的问题。生命周期是一种元数据,它允许您和编译器知道一个值在其当前内存位置上有效的时间有多长。这是一个重要的区别,因为这是Rust新手常犯的错误。Rust的生命周期不是对象创建和销毁之间的时间段!
打个比方,可以这样理解:在一个人的一生中,他们将居住在许多不同的地点,每个地点都有一个独特的地址。Rust的生命周期关注的是你当前所居住的地址,而不是你将来会死亡的时间(尽管死亡也会改变你的地址)。每次搬家都是相关的,因为你的地址不再有效。
还需要注意的是,生命周期不会改变你的代码;你的代码控制生命周期,而不是生命周期控制代码。简洁的说法是“生命周期是描述性的,而不是规定性的”。
让我们给`Combined::new`添加一些行号来突出显示生命周期:
{
let parent = Parent { count: 42 };
let child = Child { parent: &parent };
Combined { parent, child }
}
parent
的具体生命周期是从1到4,包括1和4(我将其表示为
[1,4]
)。
child
的具体生命周期是
[2,4]
,而返回值的具体生命周期是
[4,5]
。具体生命周期可以从零开始,这表示函数参数的生命周期或者存在于块之外的某个东西的生命周期。
请注意,
child
本身的生命周期是
[2,4]
,但它
引用了一个具有
[1,4]
生命周期的值。只要引用的值在被引用的值之前失效,这是可以的。问题出现在我们试图从块中返回
child
时。这将“超出”其自然长度的生命周期。
这个新的知识应该能解释前两个例子。第三个例子需要查看
Parent::child
的实现。很有可能,它会像这样:
impl Parent {
fn child(&self) -> Child { }
}
这使用“lifetime elision”来避免编写显式的“generic lifetime parameters”。它等同于:
impl Parent {
fn child<'a>(&'a self) -> Child<'a> { }
}
在这两种情况下,该方法表示将返回一个已经使用
self
的具体生命周期参数化的
Child
结构。换句话说,
Child
实例包含对创建它的
Parent
的引用,因此不能比该
Parent
实例存在的时间更长。
这也让我们意识到我们的创建函数存在问题。
fn make_combined<'a>() -> Combined<'a> { /* ... */ }
虽然你更有可能看到这个以不同的形式书写:
impl<'a> Combined<'a> {
fn new() -> Combined<'a> { }
}
在这两种情况下,没有通过参数提供生命周期参数。这意味着
Combined
将被参数化的生命周期没有受到任何限制 - 它可以是调用者想要的任何值。这是荒谬的,因为调用者可以指定
'static
生命周期,而没有办法满足这个条件。
如何修复这个问题?
最简单且最推荐的解决方案是不要尝试将这些项放在同一个结构中。通过这样做,您的结构嵌套将模拟您代码的生命周期。将拥有数据的类型放在一起,并提供允许您获取引用或包含引用的对象的方法。
有一种特殊情况下生命周期跟踪过于热衷:当您将某些东西放在堆上时。例如,当您使用
Box<T>
时。在这种情况下,被移动的结构包含一个指向堆的指针。指向的值将保持稳定,但指针本身的地址将移动。实际上,这并不重要,因为您始终会遵循指针。
有些容器提供了表示这种情况的方法,但它们要求基地址
永远不变。这就排除了可变向量,因为它们可能导致重新分配和堆分配值的移动。
使用Rental解决问题的示例:
在其他情况下,您可能希望转向某种引用计数的方式,例如使用
Rc
或
Arc
。
更多信息
引用:
将
parent
移入结构体后,为什么编译器无法获取对
parent
的新引用并将其分配给结构体中的
child
?
理论上可以这样做,但这样做会引入大量的复杂性和开销。每次移动对象时,编译器都需要插入代码来“修复”引用。这意味着复制结构体不再是一个仅仅移动一些位的廉价操作。这甚至可能意味着像这样的代码是昂贵的,这取决于一个假设的优化器有多好。
let a = Object::new();
let b = a;
let c = b;
不是强制每次移动都发生这种情况,程序员可以通过创建方法来选择何时发生这种情况,只有在调用它们时才会使用适当的引用。
具有对自身的引用的类型
有一种特殊情况,您可以创建一个具有对自身的引用的类型。但是,您需要使用类似于Option
的东西来分两步完成:
#[derive(Debug)]
struct WhatAboutThis<'a> {
name: String,
nickname: Option<&'a str>,
}
fn main() {
let mut tricky = WhatAboutThis {
name: "Annabelle".to_string(),
nickname: None,
};
tricky.nickname = Some(&tricky.name[..4]);
println!("{:?}", tricky);
}
这确实可以工作,在某种意义上,但所创建的值受到极大限制 - 它永远无法移动。值得注意的是,这意味着它无法从函数中返回,也无法按值传递给任何东西。构造函数也存在与上述生命周期相同的问题。
fn creator<'a>() -> WhatAboutThis<'a> { }
如果你尝试使用方法来编写相同的代码,你会需要那个诱人但最终无用的
&'a self
。当涉及到这个时,这段代码会更加受限制,而且在第一次方法调用后你会遇到借用检查器错误。
#[derive(Debug)]
struct WhatAboutThis<'a> {
name: String,
nickname: Option<&'a str>,
}
impl<'a> WhatAboutThis<'a> {
fn tie_the_knot(&'a mut self) {
self.nickname = Some(&self.name[..4]);
}
}
fn main() {
let mut tricky = WhatAboutThis {
name: "Annabelle".to_string(),
nickname: None,
};
tricky.tie_the_knot();
}
另请参阅:
Pin
怎么样?
Pin
,在Rust 1.33中稳定,其模块文档中有如下说明:
一个典型的例子是构建自引用结构体,因为移动具有指向自身的指针的对象将使其无效,这可能导致未定义的行为。
需要注意的是,“自引用”并不一定意味着使用引用。实际上,自引用结构体的示例明确指出(重点是我的):
我们无法通过普通引用告知编译器这一点,因为这种模式无法用常规的借用规则来描述。相反,我们使用一个原始指针,尽管我们知道它不会为空,因为我们知道它指向的是字符串。
自Rust 1.0以来,使用原始指针进行此操作的能力就已经存在了。实际上,owning-ref和rental在底层使用原始指针。
Pin结构体所添加的唯一功能是一种常见的方式来声明给定的值保证不会移动。
另请参阅:
- 如何在自引用结构中使用Pin结构体?
Parent
和Child
可能会有所帮助... - Matthieu M.