为什么我不能在同一个结构中存储一个值和一个对这个值的引用?

我有一个价值,我想存储这个价值,并在我的types的价值内引用的价值:

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) } 

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

    我们来看一个简单的实现 :

     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 p = Parent { count: 42 }; let c = Child { parent: &p }; Combined { parent: p, child: c } } } fn main() {} 

    这将失败,稍微清理错误:

     error: `p` does not live long enough --> src/main.rs:17:34 | 17 | let c = Child { parent: &p }; | ^ | note: reference must be valid for the lifetime 'a as defined on the block at 15:21... --> src/main.rs:15:22 | 15 | fn new() -> Self { | ^ note: ...but borrowed value is only valid for the block suffix following statement 0 at 16:37 --> src/main.rs:16:38 | 16 | let p = Parent { count: 42 }; | ^ 

    要完全理解这个错误,你必须考虑如何在内存中表示值,以及在移动这些值时会发生什么。 让我们注释Combined::new与一些假设的内存地址,显示值的位置:

     let p = Parent { count: 42 }; // `p` lives at address 0x1000 and takes up 4 bytes // The value of `p` is 42 let c = Child { parent: &p }; // `c` lives at address 0x1010 and takes up 4 bytes // The value of `c` is 0x1000 Combined { parent: p, child: c } // The return value lives at address 0x2000 and takes up 8 bytes // `p` is moved to 0x2000 // `c` is ... ? 

    c应该发生什么? 如果这个值刚好像p一样被移动,那么它就会指向不再保证有一个有效值的内存。 任何其他代码块都允许在内存地址0x1000处存储值。 假设它是一个整数,访问该内存可能导致崩溃和/或安全错误,并且是Rust防止的主要types之一。

    这正是生命时间阻碍的问题。 一个生命周期是一个元数据,允许你和编译器知道一个值在当前内存位置有效的时间。 这是一个重要的区别,因为这是Rust新来者常犯的错误。 锈的寿命不是从创build对象到销毁对象之间的时间间隔!

    作为比喻,可以这样想:在一个人的生活中,他们将居住在许多不同的地点,每个地点都有一个独特的地址。 一生只关心你目前居住的地址,而不关心你将来死于何时(尽pipe死亡也会改变你的地址)。 每次移动它都是相关的,因为你的地址不再有效。

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

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

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

    p具体寿命是从1到4,包括(我将表示为[1,4] )。 c的具体寿命为[2,4] ,返回值的具体寿命为[4,5] 。 可能有从零开始的具体生命周期 – 这将代表一个函数的参数生命周期,或者代表该块之外存在的某个参数的生命周期。

    请注意, c本身的寿命是[2,4] ,但它是指一个寿命为[1,4] 。 只要引用值在引用值之前变得无效,就没有问题。 当我们尝试从块中返回c时会发生问题。 这会“延长”超过其自然长度的寿命。

    这个新知识应该解释前两个例子。 第三个需要查看Parent::child的实现。 机会是,它会看起来像这样:

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

    这使用lifetime elision来避免写明确的通用生命期参数 。 这相当于:

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

    在这两种情况下,该方法都表示将返回一个Child结构,该结构已经用self的具体生命周期进行了参数化。 换句话说, Child实例包含对创build它的Parent的引用,因此不能比Parent实例生存得更长。

    这也让我们认识到,我们的创造function真的是错误的:

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

    虽然你更有可能看到这样写在一个不同的forms:

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

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

    我该如何解决?

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

    有一个特殊的情况,就是终身追踪过度:当你在堆上放置什么东西的时候。 例如,当您使用Box<T>时会发生这种情况。 在这种情况下,被移动的结构包含一个指向堆的指针。 指针值将保持稳定,但指针本身的地址将会移动。 在实践中,这并不重要,因为你总是遵循指针。

    出租箱或owning_ref箱是表示这种情况的方法,但是它们要求基地址不动 。 这排除了向量的变异,这可能导致重新分配和移动堆分配的值。

    更多信息

    p移动到结构中之后,为什么编译器无法获得对p的新引用并将其分配给结构中的c

    尽pipe在理论上可以做到这一点,但这样做会带来很大的复杂性和开销。 每次移动对象时,编译器都需要插入代码来“修正”引用。 这意味着复制一个结构不再是一个非常便宜的操作,只是移动一些位。 这甚至可能意味着像这样的代码是昂贵的,这取决于假设的优化器的效果如何:

     let a = Object::new(); let b = a; let c = b; 

    而不是强迫这一切都发生在每一个动作上,程序员可以通过创build方法来select何时发生,只有在你调用它们的时候才会采用适当的引用。


    有一个特定的情况,你可以创build一个引用自己的types。 你需要使用像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> { // ... } 

    导致非常类似的编译器消息的稍微不同的问题是对象生存期依赖性,而不是存储显式引用。 一个例子是ssh2库。 当开发比testing项目更大的东西时,试图将从该会话获得的SessionChannel彼此并置到结构中是隐藏实现细节的诱惑。 但是请注意, Channel定义在其types注释中具有'sess生命周期”,而Session没有。

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

    解决这个问题的一种方法就是在调用者之外声明Session ,然后在结构中注释一个具有生命周期的引用,类似于这个Rust用户论坛post中的回答,在封装的同时讨论相同的问题SFTP。 这不会看起来优雅,并不总是适用 – 因为现在你有两个实体来处理,而不是你想要的一个!

    从另一个答案中发现出租箱或owning_ref箱子也是这个问题的解决scheme。 让我们考虑owning_ref,它具有特定的目的: OwningHandle 。 为了避免潜在的对象移动,我们使用Box将它分配到堆上,这给了我们以下可能的解决scheme:

     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引用取消引用,因此将其存储在结构中时,我们将其命名为。 注意:这只是我的理解。 我怀疑这可能是不正确的,因为它似乎与OwningHandle不安全的讨论相当接近。

    这里有一个奇怪的细节是, Session逻辑上与TcpStream具有类似的关系,因为Channel必须Session ,但是它的所有权不被采用,并且在这种情况下没有types注释。 相反,由用户来处理这个问题,因为握手方法的文档说:

    这个会话没有取得所提供的套接字的所有权,build议确保套接字持续这个会话的生存期,以确保通信正确执行。

    强烈build议所提供的数据stream在本次会议期间不在其他地方同时使用,因为它可能会干扰协议。

    所以用TcpStream用法,完全取决于程序员来保证代码的正确性。 使用OwningHandle ,使用unsafe {}区块来吸引“危险的魔法”发生的位置。

    这个问题的另一个更高层次的讨论是在这个Rust用户论坛的主题 – 这包括一个不同的例子,它的解决scheme使用出租箱,它不包含不安全的块。