您代码中的主要问题是在结构体中使用了
'static
生命周期。为了解释生命周期是什么、它们如何工作以及您面临的错误原因,我将尝试进行说明。我提醒您这可能会很长,并且您可能会有疑问,因此最后我将链接一个非常好的视频,其中精彩地解释了生命周期。
生命周期是什么?
首先,我假设您已经查阅了一些 Rust 基本术语,例如借用、移动和 Rust 的所有权机制。如果没有,我强烈建议您阅读
Rust Book中的
理解所有权部分。
基本上,生命周期由 Rust 编译器用于定义引用在程序中存在多长时间。假设我们有以下代码(摘自该书):
{
let r;
{
let x = 4;
r = &x;
}
println!("r: {}", r);
}
由于对x的引用超出了变量的生命周期,上述代码将无法编译通过。这意味着,当内部作用域结束时,x将被丢弃,但你正在保存一个对它在外部作用域的引用。因此,当你到达println!
时,基本上你有一个对已经“不存在”的变量的引用。
更容易理解的方法是说,r
的生命周期比x
更长,因此你不能将x
的引用保存在r
中,因为在某些时候x
会消失,存储在r中的引用也将无效。
为了跟踪这些错误,Rust编译器使用标识符。这些标识符可以以'
开头后跟任何名称。因此,'a
和'potato
都是有效的生命周期名字。在Rust中,所有引用都有一个生命周期,该生命周期由它们存在的时间(它们所处的作用域)确定。
例如,在上面的代码中,有两个生命周期:
{
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
当 'a
继承于 'b
时,您不能将 &'b
引用保存到 'a
生命周期中。
生命周期省略
现在您可能会问自己为什么很少看到生命周期注释,这称为生命周期省略。这是 Rust 编译器为您做了一些工作,以便您专注于编程而不是对程序中的所有引用进行注释。例如,给定以下函数:
fn takes_a_ref(name: &str) {
...
}
Rust编译器会自动为函数括号所对应的作用域定义一个新的生命周期名称。你可以使用几乎任何名称进行注释,但编译器使用字母表中的字母来定义新的生命周期名称以保持简单。假设编译器选择字母'a
,那么这个函数将自动注释为:
fn takes_a_ref<'a>(name: &'a str) {
...
}
这意味着
takes_a_ref
的生命周期被称为
'a
,您传递给
takes_a_ref
的引用必须指向一个变量,其生存期至少与
'a
(函数)一样长。
大多数情况下编译器会自动完成这项工作,但有时您需要手动定义生命周期,比如在结构体中。
pub struct MyStruct {
pub field: &str
}
应该被注释为:
pub struct MyStruct<'a> {
pub field: &'a str,
}
特殊生命周期名称
你可能注意到我一直在谈论几乎任何名称,以指代命名生命周期的可能性。这是因为存在一些保留的生命周期名称具有特殊含义:
'static
生命周期对应于整个程序的生命周期。这意味着,为了获得具有 'static
生命周期的引用,它指向的变量必须从程序启动时开始一直存在到程序结束。一个例子是 const
变量:
const MY_CONST: &str = "Hello! ";
'_'
生命周期被称为匿名生命周期,它只是一个标记,用于指示在变量中发生了生命周期省略。编译器将在编译时替换它,它仅用于澄清。
你的代码有什么问题?
所以你遇到了以下情况:
- 你创建了一个名为
Agent
的结构体,其中包含一个 HashMap
。
- 这个
HashMap
包含一个拥有所有权的 String
和一个对 Item
的引用。
- 编译器告诉你必须指定
Item
的生命周期,因为编译器不会省略结构体中的生命周期。
- 你已经使用
'static
生命周期注释了 Item
。
- 然后你被迫在
take_item
函数中传递一个 'static
引用,因为有时你可能会将该项保存在结构体的 HashMap
中,这现在需要 Item
的 'static
生命周期。
这意味着对 Item
的引用必须指向整个程序的实例。例如:
fn function() {
let mut agent = Agent::new();
let my_item = Item::new();
let result = agent.take_item(&item);
...
}
fn main() {
function();
}
只要程序存在,你不需要my_item
一直存在。但是如果要存储在Agent
内部的任何引用,则需要与Agent
同生命周期。
解决方案(选项1)
给Agent
标注一个非'static
生命周期的生存期,例如:
pub struct Agent<'a> {
pub items: HashMap<String, &'a Item>,
}
impl <'a> Agent<'a> {
pub fn take_item(&mut self, item: &'a Item) -> std::result::Result<(), TestError> {
...
}
}
这意味着只要引用指向的实例的生命周期与存储它的 Agent
实例的生命周期一样长或更长,就不会出现问题。在 take_item
函数中,您需要指定:
引用指向的变量的生命周期必须等于或长于此 Agent
的生命周期。
fn function() {
let mut agent = Agent::new();
let my_item = Item::new();
let result = agent.take_item(&item);
...
}
fn main() {
function();
}
现在可以成功编译。
请注意,您可能需要开始注释函数,以便将该项强制保留与代理程序一样长。
在书中了解更多关于生命周期的内容。
解决方案(选项2)
您是否实际上需要该项存储为Agent
内的引用?如果答案是“否”,则可以将Item
的所有权传递给代理:
pub struct Agent {
pub items: HashMap<String, Item>,
}
在实现中,函数的生命周期会自动忽略,生存期与函数一样长:
pub fn take_item(&mut self, item: &Item) {
...
}
这就是它了。 在YouTube频道Let's Get Rusty中有一个视频,其中解释了生命周期。
static
”表示该项存在于程序运行的整个时间期间,只有在程序退出前不会被释放才是有效的。因此,除非您将要传递到“item”插槽中的内容是以确保它们永远不会被释放的方式定义/分配的,否则它显然不是正确的选择。” - Charles Duffytake_item()
的声明。 - Chayim Friedman'a
这样的生命周期的意义在于它们告诉您,某些东西被定义为具有与在相同上下文中使用'a
的其他东西相同的生命周期。如果没有其他东西使用'a
,那么在单个位置使用'a
实际上并不添加任何信息;而如果您将'a
用于参数和返回值,则表示返回值获得与参数相同的生命周期。 - Charles Duffy