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

gro*_*egg 6 reference lifetime rust borrow

Rust 新手,试图自学等等。我陷入了一生的问题。我能找到的最接近的已发布问题是:

参数要求借用 _ 来表示“静态” - 我该如何解决这个问题?

我正在玩的小项目定义了两个结构,Agent并且Item

Agent除其他内容外,该结构还包含以下行:

pub inventory: HashMap<String, &'static Item>,

此外,我还实现了这段代码:

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 {});
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我写了一个单元测试,其中包括这一行

let result = test_agent.take_item(&test_item);
Run Code Online (Sandbox Code Playgroud)

我知道某个地方有错误,因为编译器告诉我:

  --> 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`
...
Run Code Online (Sandbox Code Playgroud)

我将其test_item作为参考传递take_item()(或者更确切地说,如果我正确使用术语,则为take_item()“借用” )。test_item这似乎是我的错误的根源,但在我链接的之前的帖子中,作者能够通过调整包含引用的 Option<> 的生命周期来解决问题,据我所知。在我的示例中,我仅使用对test_item. 包含它,就像其他作者所做的那样,是推荐的方法吗?

生命周期'static意味着test_item只要单元测试正在运行,该生命周期就基本上存在?

我认为我的主要问题归结为,必须take_item()借用什么语法test_item才能使我的代码正确?我的想法正确吗?

感谢您的任何建议。

Pau*_*AVA 18

代码中的主要问题是您'static在结构中使用了生命周期。

我将尝试解释什么是生命周期、它们如何工作以及为什么您会遇到此错误。我警告你,这会很长,你可能会有疑问,所以最后我将链接一个非常好的视频,其中对生命周期进行了精彩的解释。

什么是生命周期?

首先,我假设您已经查找了一些基本的 Rust 术语,例如借用和移动以及 Rust 的所有权如何运作。如果没有,我强烈建议您阅读Rust Book中的理解所有权部分。

因此,基本上,Rust 编译器使用生命周期来定义引用在程序中的生存时间。假设我们有以下代码(摘自书中):

{
    let r;
    {
        let x = 4;
        r = &x;
    }
    println!("r: {}", r);
}
Run Code Online (Sandbox Code Playgroud)

上面的代码不会编译,因为对 x 的引用比变量的寿命长。这意味着x当到达内部作用域的末尾时, while 将被删除,您将在外部作用域中保存对它的引用。因此,当您到达基本上时,println!您将引用一个不再“存在”的变量。

理解这一点的一种更简单的方法是,r生命周期长于,因此您无法保存intox的引用,因为在某个时刻将会死亡,并且存储在 r 中的引用将无效。xrx

  • r 寿命比 x
  • r 比寿命长 x

为了跟踪这些错误,Rust 编译器使用标识符。它们几乎可以有任何前面带有'. 'a有效的生命周期也是如此,例如'potato。Rust 中的所有引用都有一个生命周期,该生命周期由它们的生存时间(它们所在的范围)决定。

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

{
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+
Run Code Online (Sandbox Code Playgroud)

因此,当生命'a周期结束时'b,您无法将&'b引用保存到生命周期中'a

终身省略

现在你可能会问自己为什么你不经常看到生命周期注释,这称为生命周期省略,这是一个 rust 编译器为你做一些工作的过程,这样你就可以专注于编程而不是注释中的所有引用你的程序。例如,给定以下函数:

fn takes_a_ref(name: &str) {
    ...
}
Run Code Online (Sandbox Code Playgroud)

rust 编译器会自动为函数括号对应的作用域定义一个新的生命周期名称。您可以使用几乎任何名称来注释它,但为了简单起见,编译器使用字母表中的字母来定义新的生命周期名称。假设编译器选择了字母,'a那么该函数将自动注释为:

fn takes_a_ref<'a>(name: &'a str) {
    ...
}
Run Code Online (Sandbox Code Playgroud)

takes_a_ref这意味着调用了 的生命周期'a,并且传递给的引用必须指向一个至少与(函数)takes_a_ref一样长的变量。'a

大多数情况下,编译器会自动为您执行此操作,但有时您必须手动定义生命周期,例如在结构中。

pub struct MyStruct {
    pub field: &str
}
// Does not compile
Run Code Online (Sandbox Code Playgroud)

应注释为:

pub struct MyStruct<'a> {
    pub field: &'a str,
}
Run Code Online (Sandbox Code Playgroud)

特殊的一生名字

您可能已经注意到,在提到命名生命周期的可能性时,我几乎讨论了任何名称。这是因为存在一些具有特殊含义的保留生命周期名称:

  • 'static
  • '_

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

const MY_CONST: &str = "Hello! "; // Here MY_CONST has an elided static lifetime
Run Code Online (Sandbox Code Playgroud)

生命'_周期称为匿名生命周期,它只是一个标记,表明变量中存在生命周期省略。它将被编译器取代,编译时它只起到澄清作用。

你的代码有什么问题吗?

那么你遇到过以下情况:

  1. 您已经创建了一个名为的结构Agent,其中包含一个HashMap.
  2. HashMap包含一个拥有的String和对一个的引用Item
  3. 编译器告诉您必须指定 的生命周期,Item因为编译器不会消除结构中的生命周期。
  4. 你已经Item'static生命注解了。
  5. 然后,您被迫'static在函数中传递引用take_item,因为有时您可能会将项目保存在结构体中,HashMap而该结构体现在'static需要Item.

现在这意味着对的引用Item必须指向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.
}
Run Code Online (Sandbox Code Playgroud)

你不需要my_item像整个程序那样活得那么久,你需要my_item活得那么久Agent。这适用于将存储在 中的任何引用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> {
        ...
    }
}
Run Code Online (Sandbox Code Playgroud)

这意味着只要参考点所在的实例的寿命与Agent存储它的特定实例的寿命一样长或更长,就不会有问题。在take_item您指定的函数中:

参考点的变量的生命周期必须等于或长于 this Agent

fn function() {
    let mut agent = Agent::new();
    let my_item = Item::new();
    let result = agent.take_item(&item);
    ...
}

fn main() {
    function();
    // No problem 
}
Run Code Online (Sandbox Code Playgroud)

现在可以正常编译了。

请记住,您可能必须开始注释函数才能强制该项目与代理一样长。

在书中阅读有关生命周期的更多信息

解决方案(选项2)

您实际上需要将该项目存储为 中的参考吗Agent?如果答案是否定的,那么您可以将 的所有权传递Item给代理:

pub struct Agent {
    pub items: HashMap<String, Item>,
}
Run Code Online (Sandbox Code Playgroud)

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

pub fn take_item(&mut self, item: &Item) {
    ...
}
Run Code Online (Sandbox Code Playgroud)

所以就是这样。这里有来自 YouTube 频道 Let's Get Rusty 的视频,其中解释了生命周期。