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

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

提问人:Shepmaster 提问时间:8/31/2015 最后编辑:trentShepmaster 更新时间:10/5/2023 访问量:59656

问:

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

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

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

Rust Reference Lifetime 借用检查器

评论

2赞 Matthieu M. 8/31/2015
对于后一个例子,定义 和 可以帮助......ParentChild
2赞 Shepmaster 8/31/2015
@MatthieuM。我对此进行了辩论,但根据两个相关的问题决定反对。这些问题都没有考虑结构的定义所讨论的方法,所以我认为最好模仿它,以便人们可以更容易地将这个问题与他们自己的情况相匹配。请注意,我确实在答案中显示了方法签名。

答:

483赞 Shepmaster 8/31/2015 #1

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

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 ... ?

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

这正是终生可以防止的问题。一生是 元数据位,允许您和编译器知道 值将在其当前内存位置有效。这是一个 重要的区别,因为这是 Rust 新手常犯的错误。 Rust 生存期不是对象 创建以及何时被摧毁!

打个比方,可以这样想:在一个人的一生中,他们会 居住在许多不同的地点,每个地点都有不同的地址。一个 Rust 生命周期与您当前居住的地址有关, 不是关于你将来什么时候会死(虽然也死了 更改您的地址)。每次你搬家时,它都是相关的,因为你 地址不再有效。

同样重要的是要注意,生存期不会更改您的代码;你 代码控制生存期,你的生存期不控制代码。这 精辟的谚语是“生命是描述性的,而不是规定性的”。

让我们用一些我们将使用的行号进行注释 要突出显示生存期:Combined::new

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

具体寿命为 1 到 4,包括 1 和 4(我将 表示为 )。的混凝土生存期是 和 返回值的具体生存期为 。它 可能拥有从零开始的具体寿命 - 那会 表示函数或某项参数的生存期 存在于块外。parent[1,4]child[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> { /* ... */ }
}

在这两种情况下,该方法都表示结构将是 返回的参数化,具体生存期为 。换句话说,实例包含一个引用 创造它的人,因此不能比那个实例活得更久。ChildselfChildParentParent

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

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

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

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

在这两种情况下,都没有通过 论点。这意味着生存期将是 参数化 with 不受任何限制 - 它可以是任何内容 调用方希望它如此。这是荒谬的,因为调用方 可以指定生存期,但无法满足 条件。Combined'static

我该如何解决?

最简单和最推荐的解决方案是不要尝试将 这些项目位于同一结构中。通过这样做,您的 结构嵌套将模拟代码的生存期。地点类型 将自己的数据一起放入一个结构中,然后提供 允许您根据需要获取引用或包含引用的对象。

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

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

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

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

更多信息

进入结构后,为什么编译器无法在结构中获取新的引用并将其分配给?parentparentchild

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

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> { /* ... */ }

如果您尝试使用方法执行相同的代码,您将需要诱人但最终无用的 .当涉及这一点时,此代码将受到更多限制,并且在第一次方法调用后,您将收到 borrow-checker 错误:&'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 中稳定,在模块文档中有这个:

这种情况的一个典型例子是构建自引用结构,因为移动一个指向自身的指针的对象会使它们失效,这可能会导致未定义的行为。

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

我们不能用正常的引用来通知编译器, 因为这种模式不能用通常的借贷规则来描述。 取而代之的是,我们使用一个原始指针,尽管已知该指针不是 null, 因为我们知道它指向字符串。

自 Rust 1.0 以来,就已经存在使用原始指针进行此行为的能力。事实上,owning-ref 和 rental 在引擎盖下使用原始指针。

唯一添加到表中的是声明给定值保证不会移动的常用方法。Pin

另请参阅:

评论

1赞 Peter Hall 1/5/2016
像这样的东西(is.gd/wl2IAt)被认为是惯用的吗?即,通过方法而不是原始数据公开数据。
2赞 Shepmaster 1/5/2016
@PeterHall当然,它只是意味着拥有 拥有 .这可能有意义,也可能没有意义,具体取决于您拥有的实际类型。返回对自己内部数据的引用是非常典型的。CombinedChildParent
2赞 derekdreery 11/14/2016
堆问题的解决方案是什么?
2赞 Shepmaster 3/5/2019
@FynnBecker仍然无法存储引用和该引用的值。 主要是一种了解包含自引用指针的结构的安全性的方法。自 Rust 1.0 以来,将原始指针用于相同目的的能力就已经存在。Pin
1赞 Shepmaster 12/22/2022
@Nirmalya会搬很多次。一次从原始声明到结构,然后在返回结构时再次返回(然后可能更多,具体取决于程序后面发生的情况)。使用内部的地址同样无效。thingCombinedCombinedu32Combined
14赞 2 revs, 2 users 80%Andrew Y #2

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

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

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

事实证明,另一个答案中的租赁板条箱owning_ref板条箱也是此问题的解决方案。让我们考虑一下 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
    }
}

这段代码的结果是我们不能再使用 了,但它与我们将要使用的 一起存储。因为对象在将对象存储在结构中时会取消引用 ,所以当它存储在结构中时,我们将其命名为 。注意:这只是我的理解。我怀疑这可能不正确,因为它似乎非常接近OwningHandle 不安全性的讨论SessionChannelOwningHandleBoxChannel

这里有一个奇怪的细节是,逻辑上与 as has to 有类似的关系,但它的所有权没有被采用,也没有类型注释。相反,由用户来处理这个问题,正如握手方法的文档所说:SessionTcpStreamChannelSession

此会话不获取所提供的套接字的所有权,它是 建议确保套接字在此生存期内持续存在 会话,以确保正确执行通信。

还强烈建议不要使用提供的流 在本届会议期间在其他地方同时举行 干扰协议。

所以用在用法上,完全取决于程序员来保证代码的正确性。有了 ,对“危险魔法”发生位置的注意力是用方块吸引的。TcpStreamOwningHandleunsafe {}

关于这个问题的进一步和更高层次的讨论是在这个 Rust 用户论坛的帖子中 - 其中包括一个不同的示例及其使用租赁板条箱的解决方案,该板条箱不包含不安全的块。

5赞 Mika Vatanen 1/20/2022 #3

我发现(只读)或(带锁定的读写)模式有时在性能和代码复杂性(主要由生命周期注释引起)之间非常有用的权衡。ArcArc<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 上使用互斥锁?RwLock)

1赞 Venryx 7/10/2022 #4

作为 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_simplemain_complex

无论如何,它完成了我需要的样板的封装/简化(因为我有许多函数需要创建 Transaction/“子”对象,现在它们只需要那个通用的锚对象创建行),同时避免了使用全新库的需要。

这些是我知道的可能相关的库:

但是,我浏览了它们,它们似乎都有这样或那样的问题(多年未更新,提出了多个不健全的问题/疑虑等),所以我犹豫是否使用它们。

因此,虽然这不是一个通用的解决方案,但我想我会为具有类似用例的人提及它:

  • 其中调用方只需要返回“子”对象。
  • 但是被调用的函数需要构造一个“父”对象来执行其功能。
  • 借用规则要求将“父”对象存储在“make_parent_and_child”函数之外的某个位置。(就我而言,这是一个函数)start_transaction