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

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

问题描述

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

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?

推荐答案

我们来看看 一个简单的实现:

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

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 新手常犯的错误.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 { /* ... */ }
}

这使用 lifetime elision 来避免编写显式的泛型生命周期参数.相当于:

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 结构将是返回已参数化的具体生命周期自己.换句话说,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.在这种情况下,移动的结构包含一个指向堆的指针.指向的值将保留稳定,但指针本身的地址会移动.在实践中,这没关系,因为你总是跟着指针走.

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.

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

Some crates provide 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 (no longer maintained or suppported)
  • owning_ref
  • ouroboros

通过租赁解决的问题示例:

Examples of problems solved with Rental:

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

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

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

If you try to do this same code with a method, you'll need the alluring but ultimately useless &'a self. When that's involved, this code is even more restricted and you will get borrow-checker errors after the first method call:

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

另见:

Pin,稳定在 Rust 1.33 中,在模块文档中有这个:

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天全站免登陆