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


222

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

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

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


1
对于后者例如,定义ParentChild能帮助...
马修M.

1
@MatthieuM。我对此进行了辩论,但基于两个相关的问题,决定反对。这些问题都没有考虑结构方法的定义,因此我认为最好模仿的是人们可以更轻松地将该问题与自己的情况相匹配。请注意,我确实在答案中显示了方法签名。
Shepmaster 2015年

Answers:


244

让我们看一下这个的简单实现

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的生命周期与您当前居住的地址有关,而与您将来的死亡时间无关(尽管死亡也会改变您的地址)。每次移动都非常重要,因为您的地址不再有效。

同样重要的是要注意,生命周期不会更改您的代码。您的代码控制生命周期,您的生命周期不控制代码。俗话说的是:“生命是描述性的,而不是描述性的”。

让我们注释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<T>例如,在使用时会发生这种情况 。在这种情况下,被移动的结构包含一个指向堆的指针。指向的值将保持稳定,但是指针本身的地址将移动。实际上,这无关紧要,因为您始终遵循指针。

租赁箱(不再进行维护或者支持)owning_ref箱子是表示这种情况下的方式,但他们需要的基地址从来没有移动。这排除了变异向量,变异向量可能导致堆分配的值的重新分配和移动。

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

Pin

Pin,已在Rust 1.33中稳定,已在模块文档中提供

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

重要的是要注意,“自我引用”并不一定意味着使用引用。确实,自我引用结构示例明确指出(强调我的意思):

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

从Rust 1.0开始,就存在使用原始指针进行此行为的功能。实际上,拥有引用和租用在幕后使用了原始指针。

唯一Pin添加到该表的是一种通用方法,它可以确保给定值不会移动。

也可以看看:


1
这样的东西(is.gd/wl2IAt)被认为是惯用的吗?即,通过方法而不是原始数据公开数据。
彼得·霍尔

2
@PeterHall当然,这仅意味着Combined拥有,Child而拥有Parent。根据您的实际类型,这可能有意义,也可能没有意义。返回对您自己的内部数据的引用是很典型的。
Shepmaster '16

解决堆问题的方法是什么?
derekdreery,2016年

@derekdreery也许您可以扩展您的评论?为什么整段谈论owning_ref板条箱都不够?
Shepmaster '16

1
@FynnBecker,仍然无法存储引用和该引用的值。Pin大部分是了解包含自引用指针的结构的安全性的方法。从Rust 1.0开始,就已经存在将原始指针用于相同目的的功能。
Shepmaster

4

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

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

解决这个问题的一种简单方法是Session在调用方中声明外部,然后用生命周期注释结构中的引用,类似于此Rust用户论坛文章中的答复,即在封装SFTP时谈论相同的问题。这看起来并不优雅,并且可能并不总是适用-因为现在您有两个实体要处理,而不是您想要的一个!

列出出租箱owning_ref箱从另一个答案中也是该问题的解决方案。让我们考虑一下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将其存储在结构中时,我们将其命名为。注意:这只是我的理解。我怀疑这可能是不正确的,因为它似乎非常接近讨论OwningHandle不安全的问题

一个有趣的细节是,Session逻辑上与TcpStreamChannelSession,但其所有权不采取和周围有这样做没有类型注释。取而代之的是,这取决于用户,因为握手方法的文档中说:

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

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

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

Rust用户论坛的线程中对此问题进行了更深入,更高级的讨论-该线程包括一个不同的示例及其使用租赁板条箱的解决方案,该板条箱不包含不安全的块。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.