当借用变量时,Rust 生命周期语法

6
新手学习 Rust,尝试自学等。我被生命周期问题困住了。我找到的最接近的已发布问题是:Argument requires that _ is borrowed for 'static - how do I work round this?
我正在玩耍的小项目定义了两个结构体,Agent 和 Item。
Agent 结构体包含以下内容之一:
pub inventory: HashMap,
此外,我实现了这段代码:
impl Agent {
    
pub fn take_item(&mut self, item: &'static Item) -> std::result::Result<(), TestError> {
        if item.can_be_taken {
            if self.can_reach_item(item) {
                self.inventory.insert(item.name.to_string(), item);
                return Ok(());
            } else {
                return Err(ItemOutOfReachError {});
            }
        } else {
            return Err(ItemCannotBeTakenError {});
        }
    }
}

我已经编写了一个单元测试,其中包含以下行:

let result = test_agent.take_item(&test_item);

我知道有一个错误,因为编译器告诉我:
  --> src/agent.rs:57:47
   |
57 |             let result = test_agent.take_item(&test_item);
   |                          ---------------------^^^^^^^^^^-
   |                          |                    |
   |                          |                    borrowed value does not live long enough
   |                          argument requires that `test_item` is borrowed for `'static`
...

我将test_item作为引用传递给take_item()(或者更确切地说,如果我使用行话,take_item()正在“借用”test_item)。这似乎是我的错误源,但在我之前链接的早期帖子中,作者能够通过调整包含引用的Option<>的生命周期来解决问题,据我所知。在我的示例中,我只是使用了一个裸引用test_item。像其他作者那样包含它是推荐的方法吗? 'static生命周期意味着test_item基本上会在单元测试运行时存活,对吗?
我认为我的主要问题归结为,take_item()必须以何种语法借用test_item才能使我的代码正确?而且,我是否正确地思考了这个问题?
感谢任何建议。

3
static”表示该项存在于程序运行的整个时间期间,只有在程序退出前不会被释放才是有效的。因此,除非您将要传递到“item”插槽中的内容是以确保它们永远不会被释放的方式定义/分配的,否则它显然不是正确的选择。” - Charles Duffy
1
借用可能没问题;你需要改变 take_item() 的声明。 - Chayim Friedman
我意识到我的问题部分在于我不清楚非静态生命周期的持续时间如何定义。对我来说,'a语法很奇怪,但我正在寻求帮助我理解的资源。感谢@CharlesDuffy的起点。 - growling_egg
'a这样的生命周期的意义在于它们告诉您,某些东西被定义为具有与在相同上下文中使用'a的其他东西相同的生命周期。如果没有其他东西使用'a,那么在单个位置使用'a实际上并不添加任何信息;而如果您将'a用于参数和返回值,则表示返回值获得与参数相同的生命周期。 - Charles Duffy
你为什么要指定生命周期呢?是出现了需要指定生命周期的错误,还是基于其他原因做出了这个决定? - Herohtar
显示剩余4条评论
1个回答

17
您代码中的主要问题是在结构体中使用了'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中的引用也将无效。

  • r的生命周期x更长
  • r超过了x

为了跟踪这些错误,Rust编译器使用标识符。这些标识符可以以'开头后跟任何名称。因此,'a'potato都是有效的生命周期名字。在Rust中,所有引用都有一个生命周期,该生命周期由它们存在的时间(它们所处的作用域)确定。

例如,在上面的代码中,有两个生命周期:

{
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        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
}
// Does not compile

应该被注释为:

pub struct MyStruct<'a> {
    pub field: &'a str,
}

特殊生命周期名称

你可能注意到我一直在谈论几乎任何名称,以指代命名生命周期的可能性。这是因为存在一些保留的生命周期名称具有特殊含义:

  • 'static
  • '_

'static 生命周期对应于整个程序的生命周期。这意味着,为了获得具有 'static 生命周期的引用,它指向的变量必须从程序启动时开始一直存在到程序结束。一个例子是 const 变量:

const MY_CONST: &str = "Hello! "; // Here MY_CONST has an elided static lifetime

'_' 生命周期被称为匿名生命周期,它只是一个标记,用于指示在变量中发生了生命周期省略。编译器将在编译时替换它,它仅用于澄清。

你的代码有什么问题?

所以你遇到了以下情况:

  1. 你创建了一个名为 Agent 的结构体,其中包含一个 HashMap
  2. 这个 HashMap 包含一个拥有所有权的 String 和一个对 Item 的引用。
  3. 编译器告诉你必须指定 Item 的生命周期,因为编译器不会省略结构体中的生命周期。
  4. 你已经使用 'static 生命周期注释了 Item
  5. 然后你被迫在 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();
    // Do some other stuff. The scope of 'function' has ended and the variables dropped but the program has not ended! 'my_item' does not live for the entirety of the program.
}

只要程序存在,你不需要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();
    // No problem 
}

现在可以成功编译。

请注意,您可能需要开始注释函数,以便将该项强制保留与代理程序一样长。

在书中了解更多关于生命周期的内容

解决方案(选项2)

您是否实际上需要该项存储为Agent内的引用?如果答案是“否”,则可以将Item的所有权传递给代理:

pub struct Agent {
    pub items: HashMap<String, Item>,
}

在实现中,函数的生命周期会自动忽略,生存期与函数一样长:

pub fn take_item(&mut self, item: &Item) {
    ...
}

这就是它了。 在YouTube频道Let's Get Rusty中有一个视频,其中解释了生命周期。


1
非常有帮助的答案,非常感谢。实际上,我几乎觉得代理应该接管物品,因为这在程序的“空间”中是逻辑上发生的事情。我承认,我半信半疑地认为实际转移变量的所有权可能不是最佳实践,但您的答案帮助我更好地理解了这一切。朋友,干杯。 - growling_egg
1
很高兴我能提供帮助!如果您没有在其他任何地方使用它,大多数情况下将变量借用给结构体是没有意义的。 - PauMAVA

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接