【问题标题】:Why can't I store a value and a reference to that value in the same struct?为什么我不能在同一结构中存储值和对该值的引用?
【发布时间】:2022-12-18 12:38:33
【问题描述】:

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

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 的定义可能会有所帮助......
  • @马修姆。我对此进行了辩论,但基于两个相关问题决定反对。这些问题都没有考虑结构的定义或者有问题的方法,所以我认为最好模仿它,这样人们可以更容易地将这个问题与他们自己的情况相匹配。请注意,我在答案中显示方法签名。

标签: rust reference lifetime borrow-checker


【解决方案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 防止的主要错误类别。

这正是问题所在一生防止。一生是一个 一些元数据,让你和编译器知道一个多长时间 值将在其有效当前内存位置.那是一个 重要的区别,因为这是 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],而 返回值的具体生命周期为[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 解决的问题示例:

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

也可以看看:

Pin 呢?

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

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

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

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

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

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

也可以看看:

【讨论】:

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

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

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

一种非常简单的解决方法是在调用者外部声明Session,然后在结构中用生命周期注释引用,类似于this Rust User's Forum post中的答案在封装时谈论同样的问题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,所以在将它存储在结构中时,我们这样命名它。笔记:这只是我的理解。我怀疑这可能不正确,因为它看起来非常接近discussion of OwningHandle unsafety

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

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

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

所以有了TcpStream的用法,完全由程序员来保证代码的正确性。对于OwningHandle,使用unsafe {} 块会引起对“危险魔法”发生位置的注意。

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

【讨论】:

    【解决方案3】:

    我发现 Arc(只读)或 Arc&lt;Mutex&gt;(带锁定的读写)模式有时在性能和代码复杂性(主要由生命周期注释引起)之间进行权衡是非常有用的。

    弧:

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

    【讨论】:

      【解决方案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_simple 模式(而不是 main_complex 变体),因为在我的情况下,“父”对象只是我临时的东西(数据库“客户端”对象)必须构建以传递给“子”对象(数据库“事务”对象),以便我可以运行一些数据库命令。

      无论如何,它完成了我需要的封装/简化样板(因为我有很多函数需要创建事务/“子”对象,现在他们所需要的只是通用的锚对象创建行),同时避免需要使用一个全新的图书馆。

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

      然而,我浏览了它们,它们似乎都有这样或那样的问题(多年未更新,提出了多个不合理的问题/担忧,等等),所以我对使用它们犹豫不决。

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

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

      【讨论】:

        猜你喜欢
        • 2015-11-25
        相关资源
        最近更新 更多