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

417

我有一个值,我想在我的自定义类型中存储该值以及内部某个东西的引用:

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

在这些情况下,我得到了一个错误,指出其中一个值“生存时间不够长”。这个错误是什么意思?


2
对于后面的例子,定义ParentChild可能会有所帮助... - Matthieu M.
2
@MatthieuM。我曾经考虑过这个问题,但基于两个相关的问题我最终决定放弃。这两个问题都没有关注所涉及结构体或方法的定义,因此我认为模仿这种方式更好,这样人们就可以更轻松地将这个问题与他们自己的情况匹配。请注意,我在答案中确实展示了该方法的签名。 - Shepmaster
4个回答

481
让我们来看一下这个的简单实现:
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 ... ?

孩子应该发生什么?如果像父母一样只是移动了值,那么它将引用不再保证具有有效值的内存。任何其他代码都可以将值存储在内存地址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])。child的具体生命周期是[2,4],而返回值的具体生命周期是[4,5]。具体生命周期可以从零开始,这表示函数参数的生命周期或者存在于块之外的某个东西的生命周期。
请注意,child本身的生命周期是[2,4],但它引用了一个具有[1,4]生命周期的值。只要引用的值在被引用的值之前失效,这是可以的。问题出现在我们试图从块中返回child时。这将“超出”其自然长度的生命周期。
这个新的知识应该能解释前两个例子。第三个例子需要查看Parent::child的实现。很有可能,它会像这样:
impl Parent {
    fn child(&self) -> Child { /* ... */ }
}

这使用“lifetime elision”来避免编写显式的“generic lifetime parameters”。它等同于:
impl Parent {
    fn child<'a>(&'a self) -> Child<'a> { /* ... */ }
}

在这两种情况下,该方法表示将返回一个已经使用self的具体生命周期参数化的Child结构。换句话说,Child实例包含对创建它的Parent的引用,因此不能比该Parent实例存在的时间更长。
这也让我们意识到我们的创建函数存在问题。
fn make_combined<'a>() -> Combined<'a> { /* ... */ }

虽然你更有可能看到这个以不同的形式书写:
impl<'a> Combined<'a> {
    fn new() -> Combined<'a> { /* ... */ }
}

在这两种情况下,没有通过参数提供生命周期参数。这意味着Combined将被参数化的生命周期没有受到任何限制 - 它可以是调用者想要的任何值。这是荒谬的,因为调用者可以指定'static生命周期,而没有办法满足这个条件。
如何修复这个问题?
最简单且最推荐的解决方案是不要尝试将这些项放在同一个结构中。通过这样做,您的结构嵌套将模拟您代码的生命周期。将拥有数据的类型放在一起,并提供允许您获取引用或包含引用的对象的方法。
有一种特殊情况下生命周期跟踪过于热衷:当您将某些东西放在堆上时。例如,当您使用Box<T>时。在这种情况下,被移动的结构包含一个指向堆的指针。指向的值将保持稳定,但指针本身的地址将移动。实际上,这并不重要,因为您始终会遵循指针。
有些容器提供了表示这种情况的方法,但它们要求基地址永远不变。这就排除了可变向量,因为它们可能导致重新分配和堆分配值的移动。

使用Rental解决问题的示例:

在其他情况下,您可能希望转向某种引用计数的方式,例如使用RcArc
更多信息
引用: 将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和rental在底层使用原始指针。
Pin结构体所添加的唯一功能是一种常见的方式来声明给定的值保证不会移动。
另请参阅:
- 如何在自引用结构中使用Pin结构体?

1
类似这样的做法(http://is.gd/wl2IAt)被认为是惯用的吗?也就是通过方法来暴露数据而不是原始数据。 - Peter Hall
2
@PeterHall 当然可以,这只是意味着 Combined 拥有 Child,而 Child 拥有 Parent。根据您实际拥有的类型,这可能有意义,也可能没有意义。返回对自己内部数据的引用非常典型。 - Shepmaster
2
堆问题的解决方案是什么? - derekdreery
2
@FynnBecker,仍然无法同时存储引用和该引用的值。Pin主要是一种了解包含自引用指针的结构体安全性的方法。使用原始指针实现相同目的的能力自Rust 1.0以来就已经存在。 - Shepmaster
1
@Nirmalya thing 会移动多次。一次是从原始声明到 Combined 结构体,然后再次当 Combined 结构体被返回时(并且根据程序后续的操作可能会有更多次)。在 Combined 内部使用 u32 的地址同样是无效的。 - Shepmaster
显示剩余10条评论

14
一个稍微不同的问题会导致非常相似的编译器错误信息,这个问题是对象生命周期依赖性,而不是存储显式引用。一个例子就是ssh2库。在开发比测试项目更大的东西时,尝试将从该会话获取的SessionChannel放在一个结构体中并排放置,从而隐藏用户的实现细节,这是很诱人的。然而,请注意Channel定义在其类型注释中具有'sess生命周期,而Session则没有。
这会导致类似的生命周期相关的编译器错误。
一种非常简单的解决方法是在调用者外部声明Session,然后为结构体中的引用注释生命周期,类似于这篇Rust用户论坛帖子中讨论封装SFTP时遇到的相同问题的答案。这看起来不太优雅,可能并不总是适用 - 因为现在你有两个实体要处理,而不是你想要的一个!
原来另一个答案中的rental crateowning_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,所以在将其存储在结构体中时,我们将其命名为这样。需要注意的是:这仅是我的理解。我怀疑这可能不正确,因为它似乎非常接近讨论不安全性
有一个有趣的细节是,SessionTcpStream的逻辑关系与ChannelSession的关系类似,但它的所有权没有被收回,并且在此过程中没有类型注释。相反,用户需要自己处理这个问题,正如握手方法的文档所说:

此会话不拥有所提供的套接字,建议确保套接字的生命周期与此会话相同,以确保正确执行通信。

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

使用 TcpStream 时,完全由程序员确保代码的正确性。而使用 OwningHandle,则通过 unsafe {} 块来引起对“危险魔法”发生位置的关注。
更高级别的讨论可以在 Rust 用户论坛线程 中找到,其中包括一个不同的示例及其使用 rental crate 的解决方案,该方案不包含不安全块。

5

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

Arc用于只读访问:

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
}

使用 Arc + Mutex 进行读写访问:

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 (我何时或为什么应该使用 Mutex 而不是 RwLock?)


1
作为 Rust 的新手,我遇到了与您最后一个示例类似的情况:
struct Combined<'a>(Parent, Child<'a>);

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

    Combined(parent, child)
}

最终,我通过使用这种模式解决了它:
fn make_parent_and_child<'a>(anchor: &'a mut DataAnchorFor1<Parent>) -> Child<'a> {
    // construct parent, then store it in anchor object the caller gave us a mut-ref to
    *anchor = DataAnchorFor1::holding(Parent::new());

    // now retrieve parent from storage-slot we assigned to in the previous line
    let parent = anchor.val1.as_mut().unwrap();

    // now proceed with regular code, except returning only the child
    // (the parent can already be accessed by the caller through the anchor object)
    let child = parent.child();
    child
}

// this is a generic struct that we can define once, and use whenever we need this pattern
// (it can also be extended to have multiple slots, naturally)
struct DataAnchorFor1<T> {
    val1: Option<T>,
}
impl<T> DataAnchorFor1<T> {
    fn empty() -> Self {
        Self { val1: None }
    }
    fn holding(val1: T) -> Self {
        Self { val1: Some(val1) }
    }
}

// for my case, this was all I needed
fn main_simple() {
    let anchor = DataAnchorFor1::empty();
    let child = make_parent_and_child(&mut anchor);
    let child_processing_result = do_some_processing(child);
    println!("ChildProcessingResult:{}", child_processing_result);
}

// but if access to parent-data later on is required, you can use this
fn main_complex() {
    let anchor = DataAnchorFor1::empty();
    
    // if you want to use the parent object (which is stored in anchor), you must...
    // ...wrap the child-related processing in a new scope, so the mut-ref to anchor...
    // ...gets dropped at its end, letting us access anchor.val1 (the parent) directly
    let child_processing_result = {
        let child = make_parent_and_child(&mut anchor);
        // do the processing you want with the child here (avoiding ref-chain...
        // ...back to anchor-data, if you need to access parent-data afterward)
        do_some_processing(child)
    };

    // now that scope is ended, we can access parent data directly
    // so print out the relevant data for both parent and child (adjust to your case)
    let parent = anchor.val1.unwrap();
    println!("Parent:{} ChildProcessingResult:{}", parent, child_processing_result);
}

这远非通用解决方案!但在我的情况下它起了作用,只需要使用上面的main_simple模式(而不是main_complex变体),因为在我的情况下,“父”对象只是一些临时的东西(一个数据库“Client”对象),我必须构造它来传递给“子”对象(一个数据库“Transaction”对象),以便我可以运行一些数据库命令。
无论如何,它完成了我需要的封装/简化样板文件(因为我有许多需要创建Transaction /“子”对象的函数,现在所有它们需要的是通用锚点对象创建行),同时避免了使用全新库的需要。
这些是我知道的可能相关的库: 然而,我浏览了它们,它们似乎都有一些问题(多年未更新,存在多个不安全问题/引起关注的问题等),所以我不敢使用它们。
因此,虽然这不是一个通用的解决方案,但我想提到它供具有类似用例的人使用:
  • 调用者只需要返回“子”对象。
  • 但是被调用函数需要构造一个“父”对象来执行其功能。
  • 并且借用规则要求“父”对象存储在超出“make_parent_and_child”函数的持久位置。(在我的情况下,这是一个start_transaction函数)

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