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

时间:2015-08-30 19:06:12

标签: lifetime borrow-checker rust

我有一个值,我想存储该值和引用 我自己的类型中的某个值内的东西:

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

在每种情况下,我都会收到一个值&#34;之类的错误 不能活得足够长#34;这个错误意味着什么?

2 个答案:

答案 0 :(得分:179)

让我们看一下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生存期与您目前驻留在的地址有关, 不管你将来何时会死(尽管也会死亡) 改变你的地址)。每次你移动它都是相关的,因为你的 地址不再有效。

注意生命周期更改您的代码也很重要;您的 代码控制着生命周期,你的生命周期不会控制代码。该 精辟的说法是&#34;生命时间是描述性的,而不是规定性的#34;。

让我们使用一些行号注释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]。它&#39; S 可能有从零开始的具体生命周期 - 那会 表示函数或其他参数的生命周期 在街区外面存在。

请注意child本身的生命周期为[2,4],但指的是 生命周期为[1,4]的值。这个就好了 引用值在引用值之前变为无效。该 当我们尝试从块返回child时出现问题。这个会 &#34;过扩展&#34;超过其自然长度的寿命。

这个新知识应该解释前两个例子。第三 一个需要查看Parent::child的实现。机会 是的,它看起来像这样:

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

这使用 life elision 来避免编写显式泛型 生命周期参数。它相当于:

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的生命周期 参数化isn不被任何东西约束 - 它可以是任何东西 来电者想要它。这是荒谬的,因为来电者 可以指定'static生命周期,但没有办法满足这一要求 条件。

我该如何解决?

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

有一种特殊情况,即终身追踪过于热心: 当你把东西放在堆上时。当您使用时会发生这种情况 例如Box<T>。在这种情况下,移动的结构 包含指向堆的指针。指向的值将保持不变 稳定,但指针本身的地址会移动。在实践中, 这并不重要,因为你总是按照指针。

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

使用Rental解决问题的示例:

更多信息

  

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

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

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

Pin怎么样?

在Rust 1.33中稳定的

Pinin the module documentation

  

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

重要的是要注意&#34;自我指导&#34;并不一定意味着使用参考。实际上,example of a self-referential struct具体说(强调我的):

  

我们无法通过普通参考通知编译器,   因为这种模式不能用通常的借用规则来描述。   而是我们使用原始指针,尽管已知它不是null,   因为我们知道它指向字符串。

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

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

另见:

答案 1 :(得分:3)

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

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

以一种非常简单的方式解决它的一种方法是在调用者中声明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,所以在将其存储在结构中时,我们将其命名为。{1}}。 注意:这只是我的理解。我怀疑这可能不正确,因为它似乎非常接近discussion of OwningHandle unsafety

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

  

此会话不取得所提供的套接字的所有权,它是   建议确保套接字持续使用寿命   会话以确保正确执行通信。

     

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

因此使用TcpStream,完全取决于程序员以确保代码的正确性。使用OwningHandle时,使用unsafe {}块绘制对“危险魔法”发生位置的关注。

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