313

我有一个值,我想以我自己的类型存储该值和对该值内的某些内容的引用:

struct Thing {
    count: u32,
}

struct Combined<'a>(Thing, &'a u32);

fn make_combined<'a>() -> Combined<'a> {
    let thing = Thing { count: 42 };

    Combined(thing, &thing.count)
}

有时,我有一个值,我想将该值和对该值的引用存储在同一结构中:

struct Combined<'a>(Thing, &'a Thing);

fn make_combined<'a>() -> Combined<'a> {
    let thing = Thing::new();

    Combined(thing, &thing)
}

有时,我什至没有参考价值,我得到同样的错误:

struct Combined<'a>(Parent, Child<'a>);

fn make_combined<'a>() -> Combined<'a> {
    let parent = Parent::new();
    let child = parent.child();

    Combined(parent, child)
}

在每种情况下,我都会收到一个错误,即其中一个值“寿命不够长”。这个错误是什么意思?

4

3 回答 3

366

让我们看一下这个的简单实现

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 };
// `parent` lives at address 0x1000 and takes up 4 bytes
// The value of `parent` is 42 
let child = Child { parent: &parent };
// `child` lives at address 0x1010 and takes up 4 bytes
// The value of `child` is 0x1000
         
Combined { parent, child }
// The return value lives at address 0x2000 and takes up 8 bytes
// `parent` is moved to 0x2000
// `child` is ... ?

应该child怎么办?如果该值只是按原样移动parent ,那么它将引用不再保证其中具有有效值的内存。允许任何其他代码将值存储在内存地址 0x1000 处。假设它是一个整数来访问该内存可能会导致崩溃和/或安全错误,并且是 Rust 防止的主要错误类别之一。

这正是生命周期阻止的问题。生命周期是一些元数据,它允许您和编译器知道一个值在其当前内存位置有效的时间。这是一个重要的区别,因为这是 Rust 新手常犯的错误。Rust 生命周期不是创建对象和销毁对象之间的时间段!

打个比方,这样想:在一个人的一生中,他们将居住在许多不同的地方,每个地方都有不同的地址。Rust 生命周期与您当前居住的地址有关,而不是您将来何时会死(尽管死亡也会改变您的地址)。每次您移动它都是相关的,因为您的地址不再有效。

同样重要的是要注意生命周期不会改变您的代码。你的代码控制生命周期,你的生命周期不控制代码。精辟的说法是“一生是描述性的,而不是规定性的”。

让我们用一些行号来注释Combined::new,我们将使用这些行号来突出生命周期:

{                                          // 0
    let parent = Parent { count: 42 };     // 1
    let child = Child { parent: &parent }; // 2
                                           // 3
    Combined { parent, child }             // 4
}                                          // 5

具体生命周期parent从 1 到 4,包括 1 到 4(我将表示为[1,4])。childis的具体[2,4]生命周期,返回值的具体生命周期是[4,5]。有可能有从零开始的具体生命周期 - 这将代表函数参数的生命周期或存在于块之外的东西。

请注意,child它自身的生命周期是[2,4],但它的是一个生命周期为 的值[1,4]。只要引用值在引用值之前变得无效,这很好。当我们尝试child从块中返回时,就会出现问题。这将使寿命“过度延长”,超出其自然长度。

这个新知识应该解释前两个例子。第三个需要查看Parent::child. 很有可能,它看起来像这样:

impl Parent {
    fn child(&self) -> Child { /* ... */ }
}

这使用生命周期省略来避免编写显式的通用生命周期参数。它相当于:

impl Parent {
    fn child<'a>(&'a self) -> Child<'a> { /* ... */ }
}

在这两种情况下,该方法都表示Child将返回一个结构,该结构已使用 self. 换句话说,Child实例包含对Parent创建它的引用,因此不能比该 Parent实例活得更久。

这也让我们认识到我们的创建函数确实有问题:

fn make_combined<'a>() -> Combined<'a> { /* ... */ }

尽管您更有可能看到它以不同的形式写成:

impl<'a> Combined<'a> {
    fn new() -> Combined<'a> { /* ... */ }
}

在这两种情况下,都没有通过参数提供生命周期参数。这意味着Combined将被参数化的生命周期不受任何限制——它可以是调用者想要的任何东西。这是荒谬的,因为调用者可以指定'static生命周期并且没有办法满足该条件。

我如何解决它?

最简单和最推荐的解决方案是不要尝试将这些项目放在同一个结构中。通过这样做,您的结构嵌套将模仿代码的生命周期。将拥有数据的类型放在一个结构中,然后提供允许您根据需要获取引用或包含引用的对象的方法。

有一种特殊情况是生命周期跟踪过于热心:当你有东西放在堆上时。例如,当您使用 a 时会发生这种情况 Box<T>。在这种情况下,被移动的结构包含指向堆的指针。指向的值将保持稳定,但指针本身的地址会移动。在实践中,这并不重要,因为您总是跟随指针。

一些 crate 提供了表示这种情况的方法,但它们要求基地址永远不会移动。这排除了变异向量,这可能导致堆分配值的重新分配和移动。

使用租赁解决的问题示例:

在其他情况下,您可能希望转向某种类型的引用计数,例如使用Rcor 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();

    // cannot borrow `tricky` as immutable because it is also borrowed as mutable
    // println!("{:?}", tricky);
}

也可以看看:

怎么样Pin

Pin,在 Rust 1.33 中稳定,在模块文档中有这个:

这种情况的一个主要示例是构建自引用结构,因为移动带有指向自身的指针的对象将使它们无效,这可能导致未定义的行为。

需要注意的是,“自我引用”并不一定意味着使用引用。确实,自引用结构的示例特别说明了(强调我的):

我们无法通过正常的引用通知编译器,因为这种模式不能用通常的借用规则来描述。相反,我们使用原始指针,尽管已知它不为空,因为我们知道它指向字符串。

自 Rust 1.0 以来,就已经存在使用原始指针来实现这种行为的能力。实际上,owning-ref 和 rent 使用了底层的原始指针。

唯一Pin添加到表中的是一种常见的方式来说明给定的值保证不会移动。

也可以看看:

于 2015-08-30T19:06:11.563 回答
12

导致非常相似的编译器消息的一个稍微不同的问题是对象生存期依赖性,而不是存储显式引用。一个例子是ssh2库。在开发比测试项目更大的东西时,很容易尝试将SessionChannel从该会话中获取并放在一个结构中,从而对用户隐藏实现细节。但是,请注意,Channel定义'sess在其类型注释中具有生命周期,而Session没有。

这会导致与生命周期相关的类似编译器错误。

以一种非常简单的方式解决它的一种方法是Session在调用者中声明外部,然后用生命周期来注释结构中的引用,类似于这个 Rust 用户论坛帖子中的答案,在封装 SFTP 时讨论相同的问题. 这看起来并不优雅,并且可能并不总是适用 - 因为现在您有两个实体要处理,而不是您想要的一个!

事实证明,另一个答案中的出租箱owning_ref 箱也是这个问题的解决方案。让我们考虑一下 owning_ref,它具有用于这个确切目的的特殊对象: OwningHandle. 为了避免底层对象移动,我们使用 a 在堆上分配它Box,这为我们提供了以下可能的解决方案:

use ssh2::{Channel, Error, Session};
use std::net::TcpStream;

use owning_ref::OwningHandle;

struct DeviceSSHConnection {
    tcp: TcpStream,
    channel: OwningHandle<Box<Session>, Box<Channel<'static>>>,
}

impl DeviceSSHConnection {
    fn new(targ: &str, c_user: &str, c_pass: &str) -> Self {
        use std::net::TcpStream;
        let mut session = Session::new().unwrap();
        let mut tcp = TcpStream::connect(targ).unwrap();

        session.handshake(&tcp).unwrap();
        session.set_timeout(5000);
        session.userauth_password(c_user, c_pass).unwrap();

        let mut sess = Box::new(session);
        let mut oref = OwningHandle::new_with_fn(
            sess,
            unsafe { |x| Box::new((*x).channel_session().unwrap()) },
        );

        oref.shell().unwrap();
        let ret = DeviceSSHConnection {
            tcp: tcp,
            channel: oref,
        };
        ret
    }
}

这段代码的结果是我们不能再使用Session了,但它与Channel我们将要使用的一起存储。因为OwningHandle对象解引用到Box,而解引用到Channel,当将其存储在结构中时,我们将其命名为这样。注意:这只是我的理解。我怀疑这可能不正确,因为它似乎非常接近于unsafety的讨论OwningHandle

这里一个奇怪的细节是,逻辑上与as has toSession有类似的关系,但它的所有权没有被占用,并且这样做没有类型注释。相反,这取决于用户,因为握手方法的文档说:TcpStreamChannelSession

此会话不获取提供的套接字的所有权,建议确保套接字在此会话的生命周期中保持不变,以确保正确执行通信。

还强烈建议在此会话期间不要在其他地方同时使用提供的流,因为它可能会干扰协议。

所以随着TcpStream用法,完全由程序员来保证代码的正确性。使用OwningHandle, 使用块吸引对“危险魔法”发生位置的注意unsafe {}

在这个Rust 用户论坛线程中对这个问题进行了更深入和更高级的讨论- 其中包括一个不同的示例及其使用不包含不安全块的出租箱的解决方案。

于 2017-11-13T09:01:14.230 回答
0

我发现Arc(只读)或Arc<Mutex>(带锁定的读写)模式有时在性能和代码复杂性(主要由生命周期注释引起)之间进行权衡非常有用。

弧:

use std::sync::Arc;

struct Parent {
    child: Arc<Child>,
}
struct Child {
    value: u32,
}
struct Combined(Parent, Arc<Child>);

fn main() {
    let parent = Parent { child: Arc::new(Child { value: 42 }) };
    let child = parent.child.clone();
    let combined = Combined(parent, child.clone());

    assert_eq!(combined.0.child.value, 42);
    assert_eq!(child.value, 42);
    // combined.0.child.value = 50; // fails, Arc is not DerefMut
}

弧+互斥:

use std::sync::{Arc, Mutex};

struct Child {
    value: u32,
}
struct Parent {
    child: Arc<Mutex<Child>>,
}
struct Combined(Parent, Arc<Mutex<Child>>);

fn main() {
    let parent = Parent { child: Arc::new(Mutex::new(Child {value: 42 }))};
    let child = parent.child.clone();
    let combined = Combined(parent, child.clone());

    assert_eq!(combined.0.child.lock().unwrap().value, 42);
    assert_eq!(child.lock().unwrap().value, 42);
    child.lock().unwrap().value = 50;
    assert_eq!(combined.0.child.lock().unwrap().value, 50);
}

另请参阅RwLock何时或为什么应该在 RwLock 上使用互斥锁?

于 2022-01-19T18:54:34.930 回答