为什么不能在同一结构中存储值和对该值的引用? [英] Why can't I store a value and a reference to that value in the same struct?

查看:121
本文介绍了为什么不能在同一结构中存储值和对该值的引用?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个值,我想存储该值和对 在我自己的类型的那个值里面的东西:

I have a value and I want to store that value and a reference to something inside that value in my own type:

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

有时候,我有一个值,我想存储该值和对 该值具有相同的结构:

Sometimes, I have a value and I want to store that value and a reference to that value in the same structure:

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

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

    Combined(thing, &thing)
}

有时候,我什至没有参考值,但我得到了 相同的错误:

Sometimes, I'm not even taking a reference of the value and I get the same error:

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

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

    Combined(parent, child)
}

在每种情况下,我都会收到一个错误,指出其中一个值确实 寿命不足".此错误是什么意思?

In each of these cases, I get an error that one of the values "does not live long enough". What does this error mean?

推荐答案

让我们看看这将失败,并显示以下错误:

This will fail with the error:

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 显示值所在位置的内存地址:

To completely understand this error, you have to think about how the values are represented in memory and what happens when you move those values. Let's annotate Combined::new with some hypothetical memory addresses that show where values are located:

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可以防止的主要错误类别.

What should happen to child? If the value was just moved like parent was, then it would refer to memory that no longer is guaranteed to have a valid value in it. Any other piece of code is allowed to store values at memory address 0x1000. Accessing that memory assuming it was an integer could lead to crashes and/or security bugs, and is one of the main categories of errors that Rust prevents.

这正是寿命可以防止的问题.一生是一个 一点元数据,使您和编译器可以知道 值将在其当前存储位置上有效.那是 重要区别,因为这是Rust新移民常犯的错误. 锈蚀寿命不是对象之间的时间间隔 创建并销毁它!

This is exactly the problem that lifetimes prevent. A lifetime is a bit of metadata that allows you and the compiler to know how long a value will be valid at its current memory location. That's an important distinction, as it's a common mistake Rust newcomers make. Rust lifetimes are not the time period between when an object is created and when it is destroyed!

打个比方,这样想:在一个人的一生中,他们会 居住在许多不同的位置,每个位置都有不同的地址.一种 Rust的生存期与您当前居住的地址有关, 不是关于你将来何时会死(尽管也会死) 更改您的地址).每次移动都非常重要,因为您的 地址不再有效.

As an analogy, think of it this way: During a person's life, they will reside in many different locations, each with a distinct address. A Rust lifetime is concerned with the address you currently reside at, not about whenever you will die in the future (although dying also changes your address). Every time you move it's relevant because your address is no longer valid.

同样重要的是要注意生命周期不要更改您的代码;你的 代码控制生命周期,而您的生命周期则无法控制代码.这 俗话说的是:生命是描述性的,而不是描述性的".

It's also important to note that lifetimes do not change your code; your code controls the lifetimes, your lifetimes don't control the code. The pithy saying is "lifetimes are descriptive, not prescriptive".

让我们用一些行号注释Combined::new 突出寿命:

Let's annotate Combined::new with some line numbers which we will use to highlight lifetimes:

{                                          // 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].它是 可能有从零开始的具体寿命-那会 表示函数或某项参数的生命周期 存在于街区之外.

The concrete lifetime of parent is from 1 to 4, inclusive (which I'll represent as [1,4]). The concrete lifetime of child is [2,4], and the concrete lifetime of the return value is [4,5]. It's possible to have concrete lifetimes that start at zero - that would represent the lifetime of a parameter to a function or something that existed outside of the block.

请注意,child本身的生存期为[2,4],但指的是 为有效期为[1,4]的值.只要 引用值在引用值之前无效.这 当我们尝试从块中返回child时,会发生问题.这个会 寿命超出"自然寿命.

Note that the lifetime of child itself is [2,4], but that it refers to a value with a lifetime of [1,4]. This is fine as long as the referring value becomes invalid before the referred-to value does. The problem occurs when we try to return child from the block. This would "over-extend" the lifetime beyond its natural length.

此新知识应解释前两个示例.第三 一个需要研究Parent::child的实现.机会 是的,它看起来像这样:

This new knowledge should explain the first two examples. The third one requires looking at the implementation of Parent::child. Chances are, it will look something like this:

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

这使用了终生省略来避免编写显式的泛型 寿命参数.等效于:

This uses lifetime elision to avoid writing explicit generic lifetime parameters. It is equivalent to:

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

在两种情况下,该方法都说Child结构将是 返回的参数已使用的具体寿命进行了参数化 self.换句话说,Child实例包含一个引用 到创建它的Parent,因此寿命不能超过 Parent实例.

In both cases, the method says that a Child structure will be returned that has been parameterized with the concrete lifetime of self. Said another way, the Child instance contains a reference to the Parent that created it, and thus cannot live longer than that Parent instance.

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

This also lets us recognize that something is really wrong with our creation function:

fn make_combined<'a>() -> Combined<'a> { /* ... */ }

尽管您更有可能看到用其他形式写的内容:

Although you are more likely to see this written in a different form:

impl<'a> Combined<'a> {
    fn new() -> Combined<'a> { /* ... */ }
}

在两种情况下,都不会通过 争论.这意味着Combined的生存期为 参数化为不受任何约束-可以是任何约束 呼叫者希望它成为.这是荒谬的,因为调用者 可以指定'static的生存期,无法满足要求 条件.

In both cases, there is no lifetime parameter being provided via an argument. This means that the lifetime that Combined will be parameterized with isn't constrained by anything - it can be whatever the caller wants it to be. This is nonsensical, because the caller could specify the 'static lifetime and there's no way to meet that condition.

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

The easiest and most recommended solution is to not attempt to put these items in the same structure together. By doing this, your structure nesting will mimic the lifetimes of your code. Place types that own data into a structure together and then provide methods that allow you to get references or objects containing references as needed.

在一种特殊情况下,寿命跟踪过分热心: 当您将某些东西放在堆上时.当您使用 例如,Box<T>.在这种情况下,移动的结构 包含一个指向堆的指针.指向的值将保持不变 稳定,但指针本身的地址将移动.在实践中, 没关系,因为您总是跟随指针.

There is a special case where the lifetime tracking is overzealous: when you have something placed on the heap. This occurs when you use a Box<T>, for example. In this case, the structure that is moved contains a pointer into the heap. The pointed-at value will remain stable, but the address of the pointer itself will move. In practice, this doesn't matter, as you always follow the pointer.

租金箱

The rental crate or the owning_ref crate are ways of representing this case, but they require that the base address never move. This rules out mutating vectors, which may cause a reallocation and a move of the heap-allocated values.

使用Rental解决的问题示例:

Examples of problems solved with Rental:

  • Is there an owned version of String::chars?
  • Returning a RWLockReadGuard independently from a method
  • How can I return an iterator over a locked struct member in Rust?
  • How to return a reference to a sub-value of a value that is under a mutex?
  • How do I store a result using Serde Zero-copy deserialization of a Futures-enabled Hyper Chunk?
  • How to store a reference without having to deal with lifetimes?

在其他情况下,您可能希望转到某种类型的引用计数,例如使用 .

In other cases, you may wish to move to some type of reference-counting, such as by using Rc or Arc.

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

After moving parent into the struct, why is the compiler not able to get a new reference to parent and assign it to child in the struct?

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

While it is theoretically possible to do this, doing so would introduce a large amount of complexity and overhead. Every time that the object is moved, the compiler would need to insert code to "fix up" the reference. This would mean that copying a struct is no longer a very cheap operation that just moves some bits around. It could even mean that code like this is expensive, depending on how good a hypothetical optimizer would be:

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

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

Instead of forcing this to happen for every move, the programmer gets to choose when this will happen by creating methods that will take the appropriate references only when you call them.

在一种特定情况下,您可以创建一个引用其自身的类型.不过,您需要使用类似Option的方法来分两步进行操作:

There's one specific case where you can create a type with a reference to itself. You need to use something like Option to make it in two steps though:

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

从某种意义上说,它确实有效,但是所创建的值受到严格限制-永远不能移动.值得注意的是,这意味着它不能从函数返回或按值传递给任何对象.构造函数在寿命方面显示出与上述相同的问题:

This does work, in some sense, but the created value is highly restricted - it can never be moved. Notably, this means it cannot be returned from a function or passed by-value to anything. A constructor function shows the same problem with the lifetimes as above:

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

Pin怎么样?

已在Rust 1.33中稳定的

Pin 模块文档中的 :

What about Pin?

Pin, stabilized in Rust 1.33, has this in the module documentation:

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

A prime example of such a scenario would be building self-referential structs, since moving an object with pointers to itself will invalidate them, which could cause undefined behavior.

请务必注意,自我引用"并不一定意味着使用引用.实际上,自引用结构的示例具体说(强调我的意思):

It's important to note that "self-referential" doesn't necessarily mean using a reference. Indeed, the example of a self-referential struct specifically says (emphasis mine):

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

We cannot inform the compiler about that with a normal reference, since this pattern cannot be described with the usual borrowing rules. Instead we use a raw pointer, though one which is known to not be null, since we know it's pointing at the string.

自Rust 1.0起,就已经存在使用原始指针进行此行为的功能.实际上,拥有引用和租用在幕后使用了原始指针.

The ability to use a raw pointer for this behavior has existed since Rust 1.0. Indeed, owning-ref and rental use raw pointers under the hood.

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

The only thing that Pin adds to the table is a common way to state that a given value is guaranteed to not move.

另请参阅:

这篇关于为什么不能在同一结构中存储值和对该值的引用?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆