首页 文章

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

提问于
浏览
157

我有一个值,我想在我自己的类型中存储该值以及对该值内部内容的引用:

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 回答

  • 3

    我们来看看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阻止的主要错误类别之一 .

    这正是生命周期所阻止的问题 . 生命周期是一些元数据,允许您和编译器知道值在 current memory location 处有效的时间 . 这是Rust新手们犯的一个常见错误 . Rust生命周期不是创建对象和销毁对象之间的时间段!

    作为类比,以这种方式思考:在一个人的生活中,他们将居住在许多不同的地方,每个地点都有不同的地址 . Rust生命周期与您当前居住的地址有关,而与您将来何时死亡无关(尽管死亡也会改变您的地址) . 每次移动它都是相关的,因为您的地址不再有效 .

    同样重要的是要注意,生命周期不会改变你的代码;您的代码控制着生命周期,您的生命周期不会控制代码 . 精辟的说法是"lifetimes are descriptive, not prescriptive" .

    让我们使用一些行号注释 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] ,但它是 refers to 一个生命周期为 [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<T> 时会发生这种情况 . 在这种情况下,移动的结构包含指向堆的指针 . 指向的值将保持稳定,但指针本身的地址将移动 . 在实践中,这并不重要,因为你总是按照指针 .

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

    租赁解决问题的例子:

    更多信息

    将父移动到struct之后,为什么编译器无法获得对父项的新引用并将其分配给struct中的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);
    }
    

    从某种意义上说,这确实有效,但创造的 Value 受到严格限制 - 永远无法移动 . 值得注意的是,这意味着它不能从函数返回或通过值传递给任何东西 . 构造函数显示与上述生命周期相同的问题:

    fn creator<'a>() -> WhatAboutThis<'a> { /* ... */ }
    

    Pin怎么样?

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

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

    它's important to note that 2862079 doesn' t必然意味着使用引用 . 事实上,example of a self-referential struct具体说(强调我的):

    我们无法通过正常引用通知编译器,因为无法使用通常的借用规则来描述此模式 . 相反,我们使用原始指针,虽然已知它不是null,因为我们知道它指向字符串 .

    自Rust 1.0以来,已存在使用原始指针进行此行为的能力 . 事实上,拥有参考和租赁使用引擎盖下的原始指针 .

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

    也可以看看:

  • 169

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

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

    以一种非常简单的方式解决它的一种方法是在调用者中声明 Session 外部,然后使用生命周期在结构中注释引用,类似于在封装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 ,当它存储在结构中时,我们将其命名为 . NOTE: 这只是我的理解 . 我怀疑这可能不正确,因为它似乎非常接近discussion of OwningHandle unsafety .

    这里有一个奇怪的细节是 Session 逻辑上与 TcpStream 具有类似的关系,因为 Channel 必须 Session ,但是它的所有权没有被采用,并且没有类型注释这样做 . 相反,由用户负责处理这个问题,因为handshake方法的文档说:

    此会话不取得所提供套接字的所有权,建议确保套接字持续此会话的生命周期,以确保正确执行通信 . 强烈建议所提供的流在本次会话期间不在其他地方同时使用,因为它可能会干扰协议 .

    因此,使用 TcpStream ,完全取决于程序员,以确保代码的正确性 . 使用 OwningHandle ,使用 unsafe {} 块绘制对"dangerous magic"发生位置的注意 .

    关于这个问题的进一步和更高层次的讨论在这个Rust User's Forum thread中 - 其中包括一个不同的例子及其使用租赁箱的解决方案,它不包含不安全的块 .

相关问题