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

She*_*ter 193 lifetime rust borrow-checker

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

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)
}
Run Code Online (Sandbox Code Playgroud)

有时候,我有一个值,我想在同一个结构中存储该值和对该值的引用:

struct Combined<'a>(Thing, &'a Thing);

fn make_combined<'a>() -> Combined<'a> {
    let thing = Thing::new();

    Combined(thing, &thing)
}
Run Code Online (Sandbox Code Playgroud)

有时,我甚至没有参考该值,我得到同样的错误:

struct Combined<'a>(Parent, Child<'a>);

fn make_combined<'a>() -> Combined<'a> {
    let parent = Parent::new();
    let child = parent.child();

    Combined(parent, child)
}
Run Code Online (Sandbox Code Playgroud)

在每种情况下,我都会收到一个错误,即其中一个值"活不够长".这个错误是什么意思?

She*_*ter 209

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

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() {}
Run Code Online (Sandbox Code Playgroud)

这将失败并显示错误:

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`
Run Code Online (Sandbox Code Playgroud)

要完全理解此错误,您必须考虑如何在内存中表示值以及移动 这些值时会发生什么.让我们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 ... ?
Run Code Online (Sandbox Code Playgroud)

应该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
Run Code Online (Sandbox Code Playgroud)

混凝土寿命parent是从1至4,包括(我将表示为[1,4]).具体寿命child[2,4],返回值的具体寿命是[4,5].有可能具有从零开始的具体生命周期 - 这将表示函数的参数的生命周期或者在块之外存在的东西.

请注意,child它本身的生命周期是[2,4],但它指的是具有生命周期的值[1,4].只要引用值在引用值之前变为无效,这就没问题.当我们尝试child从块返回时会出现问题.这会使寿命"过度延长"超过其自然长度.

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

impl Parent {
    fn child(&self) -> Child { /* ... */ }
}
Run Code Online (Sandbox Code Playgroud)

这使用生命周期省略来避免编写显式的通用生命周期参数.它相当于:

impl Parent {
    fn child<'a>(&'a self) -> Child<'a> { /* ... */ }
}
Run Code Online (Sandbox Code Playgroud)

在这两种情况下,该方法Child都会返回一个结构,该结构已经过具体的生命周期参数化 self.换句话说,Child实例包含对Parent创建它的引用,因此不能比该Parent实例更长寿 .

这也让我们认识到我们的创建功能确实存在问题:

fn make_combined<'a>() -> Combined<'a> { /* ... */ }
Run Code Online (Sandbox Code Playgroud)

虽然您更有可能看到以不同形式编写的内容:

impl<'a> Combined<'a> {
    fn new() -> Combined<'a> { /* ... */ }
}
Run Code Online (Sandbox Code Playgroud)

在这两种情况下,都没有通过参数提供生命周期参数.这意味着Combined将参数化的生命周期不受任何约束 - 它可以是调用者想要的任何东西.这是荒谬的,因为调用者可以指定'static生命周期,并且无法满足该条件.

我如何解决它?

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

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

租赁箱owning_ref箱子是表示这种情况下的方式,但他们需要的基地址从来没有移动.这排除了变异向量,这可能导致重新分配和移动堆分配的值.

更多信息

移动后Rc入结构,为什么编译器不能够得到一个新的参考Arc,并将其分配给parent在结构?

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

let a = Object::new();
let b = a;
let c = b;
Run Code Online (Sandbox Code Playgroud)

程序员不是强迫每一次移动发生这种情况,而是通过创建仅在调用它们时才会采用适当引用的方法来选择何时发生.


有一个特定情况,您可以创建一个引用自身的类型.你需要使用类似的东西parent分两步:

#[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);
}
Run Code Online (Sandbox Code Playgroud)

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

fn creator<'a>() -> WhatAboutThis<'a> { /* ... */ }
Run Code Online (Sandbox Code Playgroud)

  • @PeterHall肯定,它只是意味着`Combined`拥有拥有`Parent`的`Child`.根据您拥有的实际类型,这可能有意义也可能没有意义.返回对您自己的内部数据的引用是非常典型的. (2认同)
  • 堆问题的解决方案是什么? (2认同)
  • @FynnBecker 仍然无法存储 **reference** 和该引用的值。`Pin` 主要是一种了解包含自引用 **pointer** 的结构的安全性的方法。从 Rust 1.0 开始就存在将原始指针用于相同目的的能力。 (2认同)

And*_*w Y 12

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

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

一种以非常简单的方式解决它的方法是Session在调用者中声明外部,然后使用生命周期注释结构内的引用,类似于这个 Rust 用户论坛帖子中的答案,在封装 SFTP 时谈论相同的问题. 这看起来不太优雅,可能并不总是适用 - 因为现在您要处理两个实体,而不是您想要的一个!

原来其他答案中的租用板条箱owning_ref 板条箱也是此问题的解决方案。让我们考虑一下 owning_ref,它具有用于此确切目的的特殊对象: OwningHandle. 为了避免底层对象移动,我们使用 a 在堆上分配它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
    }
}
Run Code Online (Sandbox Code Playgroud)

这段代码的结果是我们不能再使用 the Session,但它与Channel我们将使用的一起存储。因为OwningHandle对象解引用到Box,解引用到Channel,所以当把它存储在一个结构体中时,我们就这样命名它。注意:这只是我的理解。我怀疑这可能不正确,因为它似乎与不安全的讨论OwningHandle非常接近。

这里一个奇怪的细节是Session逻辑上与TcpStreamas Channelhas to具有相似的关系Session,但它的所有权没有被占用,并且没有类型注释。相反,这由用户来处理,正如握手方法的文档所说:

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

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

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

关于这个问题的进一步和更高级的讨论是在这个Rust 用户论坛线程中- 其中包括一个不同的示例及其使用租用箱的解决方案,其中不包含不安全的块。


Mik*_*nen 5

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

用于只读访问的弧:

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
}
Run Code Online (Sandbox Code Playgroud)

Arc + Mutex 用于读写访问:

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);
}
Run Code Online (Sandbox Code Playgroud)

另请参阅RwLock何时或为何应使用互斥锁而不是 RwLock?


归档时间:

查看次数:

17914 次

最近记录:

6 年,3 月 前