当前位置: 首页 > 知识库问答 >
问题:

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

安承教
2023-03-14

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

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个答案

陆畅
2023-03-14

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

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

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

事实证明,另一个答案中的租赁板条箱或拥有的_ref板条箱也是这个问题的解决方案。让我们来考虑Objang-Ref,它有这个特殊目的的特殊对象:<代码> OwningHandle < /代码>。为了避免底层对象移动,我们使用在堆上分配它,这为我们提供了以下可能的解决方案:

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对象取消对框的引用,该框取消对通道的引用,因此在将其存储到结构中时,我们将其命名为。注:这只是我的理解。我怀疑这可能是不对的,因为它似乎非常接近于对OwningHandleunsafety的讨论。

这里一个奇怪的细节是,会话逻辑上与TcpStream有着类似的关系,就像频道会话一样,但是它的所有权并没有被获得,而且在这样做的时候也没有类型注释。相反,这取决于用户,正如握手方法的文档所述:

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

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

因此,对于TcpStream的使用,完全由程序员来确保代码的正确性。使用OwningHandle,使用unsafe{}块来注意“危险魔法”发生的位置。

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

弓温书
2023-03-14

让我们看一个简单的实现:

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处存储值。假设该内存是整数,则访问该内存可能会导致崩溃和/或安全漏洞,这是防止生锈的主要错误类别之一。

这正是有生之年预防的问题。生存期是位元数据,允许您和编译器知道值在其当前内存位置的有效期。这是一个重要的区别,因为这是生锈新人常犯的错误。生锈寿命不是创建对象和销毁对象之间的时间段!

打个比方,可以这样想:在一个人的一生中,他们会居住在许多不同的地方,每个地方都有不同的地址。生锈的一生与你目前居住的地址有关,而不是与你将来何时死亡有关(尽管死亡也会改变你的地址)。每次你搬家都有关系,因为你的地址不再有效。

还需要注意的是,生命周期不会改变代码;你的代码控制生命周期,你的生命周期不控制代码。俗话说得好:“人生是描述性的,不是规定性的”。

让我们用一些行号来注释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])。子项的具体生存期是[2,4],返回值的具体生存期是[4,5]。可以有从零开始的具体生命周期,这表示函数或块外存在的某个对象的参数的生命周期。

请注意,本身的生存期是[2,4],但它指的是一个生存期为[1,4]的值。只要引用值在引用值之前变得无效,这就可以了。当我们试图从块返回时,就会出现问题。这将“过度延长”寿命,超出其自然长度。

这个新知识应该可以解释前两个例子。第三个需要查看父::cild的实现。很有可能,它会看起来像这样:

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

这使用生存期省略来避免编写显式的泛型生存期参数。这相当于:

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

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

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

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

虽然您更可能看到这是以不同的形式编写的:

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

在这两种情况下,都没有通过参数提供生存期参数。这意味着,组合的将被参数化的生存期不受任何约束-它可以是调用者想要的任何东西。这是毫无意义的,因为调用者可以指定的静态生存期,而无法满足该条件。

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

有一种特殊情况,即生存期跟踪过于热心:当您在堆上放置了一些东西时。当您使用Box时,会发生这种情况

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

  • 租赁(不再维护或支持)

通过租赁解决的问题示例

  • 是否有String::chars的自有版本

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

移动到结构中后,为什么编译器不能获得对的新引用并将其分配给结构中的

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

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

如果您试图用一种方法编写相同的代码,您将需要诱人但最终无用的

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

另见:

  • 不能在一个代码中多次借用可变代码,但在另一个非常相似的代码中可以借用

引脚在Rust 1.33中稳定,在模块留档中有这个:

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

需要注意的是,“自我参照”并不一定意味着使用参照。事实上,自引用结构的示例特别指出(我的重点):

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

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

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

另见:

  • 如何将Pin结构与自引用结构一起使用

 类似资料:
  • 我有一个类,并且我只重写方法。请在java类下面找到 我创建了两个对象,并且这两个对象的所有属性值都相同,并将它们放在中。下面是代码 我得到了预期的结果。由于我只重写了hashCode()方法,并且这两个对象的哈希值是相同的,所以它们存储在HashMap表数组的相同索引位置。下面是调试模式下的预期结果。 但是,如果我没有重写hashCode()方法,而是重写equals()方法,则它们将存储在Ha

  • 在Solr围绕原子更新的文档中,他们提到一个字段应该是非索引和非存储的。 https://lucene.apache.org/solr/guide/7_6/updating-parts-of-documents.html#in-place-update-example 只有当要更新的字段满足以下三个条件时,才使用该方法执行原子更新操作: 是非索引(indexed=“false”)、非存储(stor

  • 我正在为一个项目制作购物车脚本。购物车已经创建,但现在我正在查询购物车中的产品插入订单表的位置。但是当我这样做的时候,每个产品都有自己的,有没有可能给购物车中的每个产品相同的? 将产品插入订单表的查询: SQL查询如下: 我希望有人能给我一些建议, 提前感谢!

  • 问题内容: 我想创建一个像堆栈溢出这样的问答应用程序。 我不知道如何在MySQL表列中存储多个答案。 我想知道如何在sql表的同一行中存储问题和多个答案。 问题答案: 创建两个表并使用外键关系将它们连接是最好的解决方案之一。因为将问题和答案保存在同一表格中可能不会使您对同一问题有多个答案。如果您希望一个问题有多个答案,那么数据库的冗余肯定会增加。因此,请尝试正确使用DB- Relationship

  • 如果我将数组创建为: 那么< code>number[j]中存储的值是什么,其中< code>j=8,9。如果存储了一些值,那么为什么存储这些值以及如何存储这些值??

  • 类别:账户余额 我已经把这两个类都放在Balance.java和Account tBalance.java.这两个文件都在E:/程序/MyPack. Balance.java编译没有错误但是当我编译Account tBalance.java它给出错误:找不到符号"平衡". 我无法弄清楚为什么当两个类都在同一个包中声明时? 我正在使用javac B从MyPack编译alance.javajavac