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

Posted

技术标签:

【中文标题】为啥我不能在同一个结构中存储一个值和对该值的引用?【英文标题】:Why can't I store a value and a reference to that value in the same struct?为什么我不能在同一个结构中存储一个值和对该值的引用? 【发布时间】:2021-08-27 05:45:48 【问题描述】:

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

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)

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

【问题讨论】:

对于后一个示例,ParentChild 的定义可能会有所帮助... @MatthieuM。我对此进行了辩论,但基于两个相关的问题决定反对它。这两个问题都没有考虑结构的定义有问题的方法,所以我认为最好模仿人们可以更容易地将这个问题与他们自己的情况相匹配。请注意,我确实在答案中显示了方法签名。 【参考方案1】:

我们来看看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`

要完全理解这个错误,你必须考虑如何 值在内存中表示,当您移动时会发生什么 那些价值观。让我们用一些假设来注释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 防止的主要错误类别。

这正是 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(我将 表示为[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>  /* ... */ 

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

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

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

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

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

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

我该如何解决?

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

有一种特殊情况是生命周期跟踪过于热心: 当你有东西放在堆上时。当您使用 例如Box&lt;T&gt;。在这种情况下,被移动的结构 包含指向堆的指针。指向的值将保持不变 稳定,但指针本身的地址会移动。在实践中, 这没关系,因为你总是跟着指针走。

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

rental(不再维护或支持) owning_ref ouroboros

通过租赁解决的问题示例:

Is there an owned version of String::chars? Returning a RWLockReadGuard independently from a method How can I return an iterator over a locked struct member in Rust? How to return a reference to a sub-value of a value that is under a mutex? How do I store a result using Serde Zero-copy deserialization of a Futures-enabled Hyper Chunk? How to store a reference without having to deal with lifetimes?

在其他情况下,您可能希望使用某种类型的引用计数,例如使用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>  /* ... */ 

如果您尝试使用方法执行相同的代码,您将需要诱人但最终无用的&amp;'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);

另见:

Cannot borrow as mutable more than once at a time in one code - but can in another very similar

Pin 呢?

Pin,稳定在 Rust 1.33,有这个in the module documentation:

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

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

我们无法通过普通引用通知编译器, 因为这种模式不能用通常的借用规则来描述。 取而代之的是我们使用一个原始指针,虽然它已知不为空, 因为我们知道它指向字符串。

自 Rust 1.0 以来就存在使用原始指针进行此行为的能力。事实上,owning-ref 和 renting 在后台使用原始指针。

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

另见:

How to use the Pin struct with self-referential structures?

【讨论】:

这样的东西 (is.gd/wl2IAt) 被认为是惯用的吗?即,通过方法而不是原始数据公开数据。 @PeterHall 当然,这只是意味着Combined 拥有Child 拥有Parent。根据您拥有的实际类型,这可能有意义,也可能没有意义。返回对您自己的内部数据的引用非常典型。 堆问题的解决方法是什么? @derekdreery 也许您可以扩展您的评论?为什么整段都在谈论 owning_ref 箱子不足? @FynnBecker 仍然无法存储 reference 和该引用的值。 Pin 主要是一种了解包含自引用 指针 的结构的安全性的方法。自 Rust 1.0 以来,就已经存在使用原始指针实现相同目的的能力。【参考方案2】:

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

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

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

原来另一个答案中的rental crate 或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,后者取消引用Channel,所以在将其存储在结构中时,我们将其命名为这样。 注意:这只是我的理解。我怀疑这可能不正确,因为它似乎非常接近discussion of OwningHandle unsafety。

这里有一个奇怪的细节是Session 在逻辑上与TcpStream 有类似的关系,因为Channel 必须与Session,但它的所有权并没有被占用,并且没有类型注释。相反,这取决于用户,正如handshake 方法的文档所述:

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

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

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

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

【讨论】:

以上是关于为啥我不能在同一个结构中存储一个值和对该值的引用?的主要内容,如果未能解决你的问题,请参考以下文章

为啥对 Python 值的引用(即函数参数)存储在 CPython 的堆栈(帧)中?

原始值和引用值

在c语言中子函数引用主函数中的值和引用主函数某个值的地址的区别

js中 原始值和引用值

C# 线程真的可以缓存一个值并忽略其他线程上对该值的更改吗?

JavaScript中原始值和引用值传递