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

Why can't I store a value and a reference to that value in the same struct?

我有一个值,我想存储该值和对 在我自己的类型中那个值里面的东西:

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

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

我们来看看a simple implementation of this:

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`

要完全理解此错误,您必须考虑 值在内存中表示,当您 move 时会发生什么 那些价值观。让我们用一些假设来注释 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 处的值。假设它是访问该内存 整数可能导致崩溃 and/or 安全漏洞,并且是其中之一 Rust 防止的主要错误类别。

这正是 lifetimes 防止的问题。一生是一个 一些元数据,让你和编译器知道一个多长时间 值将在其 当前内存位置 处有效。那是一个 重要的区别,因为这是 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])。 child的具体生命周期为[2,4],并且 return 值的具体生命周期是 [4,5]。它是 可能有从零开始的具体生命周期——那会 代表函数或其他东西的参数的生命周期 存在于块外。

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

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

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

这使用 lifetime elision 来避免写显式 generic 寿命参数。相当于:

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

在这两种情况下,该方法表示 Child 结构将是 returned 已经用具体的生命周期参数化了 self。换句话说,Child 实例包含一个引用 到创建它的Parent,因此不能活得比它长 Parent实例。

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

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

尽管您更有可能看到以不同形式书写的内容:

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

在这两种情况下,都没有通过 争论。这意味着 Combined 的生命周期将是 参数化不受任何限制——它可以是任何东西 来电者希望它是。这是荒谬的,因为调用者 可以指定 'static 生命周期,但没有办法满足 条件。

我该如何解决?

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

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

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

租赁解决的问题示例:

在其他情况下,您可能希望转向某种类型的引用计数,例如使用 Rc or Arc.

更多信息

After moving parent into the struct, why is the compiler not able to get a new reference to parent and assign it to child in the struct?

虽然理论上可以这样做,但这样做会带来大量的复杂性和开销。每次移动对象时,编译器都需要插入代码来“修复”引用。这意味着复制一个结构不再是一个非常便宜的操作,它只是移动一些位。这甚至可能意味着这样的代码很昂贵,具体取决于假设的优化器有多好:

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

这在某种意义上确实有效,但创造的价值受到高度限制 - 它 永远 无法移动。值得注意的是,这意味着它不能从函数 returned 或按值传递给任何东西。构造函数显示与上述生命周期相同的问题:

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, stabilized in Rust 1.33, has this in the module documentation:

A prime example of such a scenario would be building self-referential structs, since moving an object with pointers to itself will invalidate them, which could cause undefined behavior.

重要的是要注意“自我引用”并不一定意味着使用引用。事实上,example of a self-referential struct 明确表示(强调我的):

We cannot inform the compiler about that with a normal reference, since this pattern cannot be described with the usual borrowing rules. Instead we use a raw pointer, though one which is known to not be null, since we know it's pointing at the string.

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

Pin 添加到 table 的唯一内容是声明给定值保证不会移动的常用方式。

另请参阅:

导致非常相似的编译器消息的一个稍微不同的问题是对象生命周期依赖性,而不是存储显式引用。一个例子是 ssh2 library. When developing something bigger than a test project, it is tempting to try to put the Session and Channel obtained from that session alongside each other into a struct, hiding the implementation details from the user. However, note that the Channel definition has the 'sess lifetime in its type annotation, while Session 没有。

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

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

原来另一个答案中的 rental crate or the owning_ref crate 也是这个问题的解决方案。让我们考虑一下 owning_ref,它具有用于此确切目的的特殊对象: OwningHandle。为了避免底层对象移动,我们使用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,而 Box 取消引用 Channel,当将其存储在结构中时,我们将其命名为这样。 注意:这只是我的理解。我怀疑这可能不正确,因为它看起来非常接近 discussion of OwningHandle unsafety

这里有一个奇怪的细节是,Session 在逻辑上与 TcpStream 的关系与 ChannelSession 的关系相似,但它的所有权并未被占用,并且存在这样做没有类型注释。相反,由用户来处理这个问题,正如 handshake 方法的文档所说:

This session does not take ownership of the socket provided, it is recommended to ensure that the socket persists the lifetime of this session to ensure that communication is correctly performed.

It is also highly recommended that the stream provided is not used concurrently elsewhere for the duration of this session as it may interfere with the protocol.

所以有了TcpStream的用法,完全由程序员来保证代码的正确性。使用 OwningHandle,注意 "dangerous magic" 发生的位置是使用 unsafe {} 块绘制的。

在此 Rust User's Forum thread 中对这个问题进行了更深入、更高级的讨论 - 其中包括一个不同的示例及其使用不包含不安全块的出租箱的解决方案。

我发现 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 (When or why should I use a Mutex over an RwLock?)