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

2023-12-30

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

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

在每种情况下,我都会收到一个错误,其中一个值“确实 活得不够长”。这个错误是什么意思?


让我们看看:

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`

要完全理解这个错误,你必须考虑如何 值在内存中表示,当您move这些价值观。我们来注释一下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 的寿命是not对象出现之间的时间间隔 创建和销毁的时间!

打个比方,这样想:在人的一生中,他们会 驻留在许多不同的位置,每个位置都有不同的地址。 A Rust 的生命周期与你的地址有关目前居住在, 不是关于你将来什么时候会死(尽管也会死) 更改您的地址)。每次你搬家都是相关的,因为你的 地址不再有效。

同样重要的是要注意生命周期do not更改您的代码;你的 代码控制生命周期,你的生命周期并不控制代码。这 精辟的说法是“生命是描述性的,而不是规定性的”。

我们来注释一下Combined::new以及我们将使用的一些行号 突出显示生命周期:

{                                          // 0
    let parent = Parent { count: 42 };     // 1
    let child = Child { parent: &parent }; // 2
                                           // 3
    Combined { parent, child }             // 4
}                                          // 5

The 具体寿命 of parent是从 1 到 4,包含这两个值(我会 表示为[1,4])。具体寿命child is [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将 参数化 with 不受任何限制 - 它可以是任何东西 呼叫者希望如此。这是无意义的,因为调用者 可以指定'static一生都没有办法满足 健康)状况。

我如何解决它?

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

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

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

  • rental https://crates.io/crates/rental(不再维护或支持)
  • 拥有_参考 https://crates.io/crates/owning_ref (has 多重稳健性问题 https://github.com/noamtashma/owning-ref-unsoundness)
  • 衔尾蛇 https://crates.io/crates/ouroboros
  • 自身细胞 https://crates.io/crates/self_cell
  • yoke https://crates.io/crates/yoke

租赁解决的问题示例:

  • String::chars 是否有自有版本? https://stackoverflow.com/q/47193584/155423
  • 独立于方法返回 RWLockReadGuard https://stackoverflow.com/q/50496879/155423
  • 如何在 Rust 中返回锁定结构成员的迭代器? https://stackoverflow.com/q/51664098/155423
  • 如何返回对互斥体下值的子值的引用? https://stackoverflow.com/q/40095383/155423
  • 如何使用支持 Futures 的超级块的 Serde 零拷贝反序列化来存储结果? https://stackoverflow.com/q/43702185/155423
  • 如何存储引用而不必处理生命周期? https://stackoverflow.com/q/49300618/155423

在其他情况下,您可能希望转向某种类型的引用计数,例如使用Rc https://doc.rust-lang.org/std/rc/struct.Rc.html or Arc https://doc.rust-lang.org/std/sync/struct.Arc.html.

更多信息

搬家后parent进入结构体,为什么编译器无法获得新的引用parent并将其分配给child在结构中?

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

let a = Object::new();
let b = a;
let c = b;

而不是强迫这种情况发生every移动,程序员得到choose通过创建仅在调用它们时才会采用适当引用的方法来实现这种情况。

具有自身引用的类型

有一种具体情况,您can创建一个引用自身的类型。你需要使用类似的东西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);
}

从某种意义上来说,这确实有效,但创造的价值受到严格限制——它可以never被移动。值得注意的是,这意味着它不能从函数返回或按值传递给任何东西。构造函数显示了与上面相同的生命周期问题:

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

如果您尝试使用方法执行相同的代码,您将需要诱人但最终无用的方法&'a self。当涉及到这一点时,此代码会受到更多限制,并且在第一个方法调用后您将收到借用检查器错误:

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

也可以看看:

  • 在一个代码中不能一次多次借用可变代码 - 但可以在另一个非常相似的代码中借用可变的 https://stackoverflow.com/q/31067031/155423

关于什么Pin?

Pin https://doc.rust-lang.org/std/pin/struct.Pin.html,在 Rust 1.33 中稳定,有这个在模块文档中 https://doc.rust-lang.org/std/pin/index.html:

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

重要的是要注意“自我引用”并不一定意味着使用一个参考。确实,自引用结构的示例 https://doc.rust-lang.org/std/pin/index.html#example-self-referential-struct具体说(强调我的):

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

自 Rust 1.0 以来就已经存在使用原始指针来实现此行为的能力。事实上,拥有引用和租赁在幕后使用原始指针。

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

也可以看看:

  • 如何将 Pin 结构与自引用结构一起使用? https://stackoverflow.com/q/49860149/155423
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

随机推荐