为什么生命周期的链接只对可变引用有影响?

26

几天前,有人在 一个问题 中遇到了关于可变引用和包含借用数据类型的关联生命周期的问题。问题是向该类型提供具有与类型内部借用数据相同的生命周期的引用。

struct VecRef<'a>(&'a Vec<u8>);

struct VecRefRef<'a>(&'a mut VecRef<'a>);

fn main() {
    let v = vec![8u8, 9, 10];
    let mut ref_v = VecRef(&v);
    create(&mut ref_v);
}

fn create<'b, 'a>(r: &'b mut VecRef<'a>) {
    VecRefRef(r);
}

示例代码

我在create()函数中显式标注了 'b。但这段代码无法编译:

error[E0623]: lifetime mismatch
  --> src/main.rs:12:15
   |
11 | fn create<'b, 'a>(r: &'b mut VecRef<'a>) {
   |                      ------------------
   |                      |
   |                      these two types are declared with different lifetimes...
12 |     VecRefRef(r);
   |               ^ ...but data from `r` flows into `r` here

生命周期'b类似于'b < 'a,因此违反了VecRefRef<'a>的约束条件,该条件要求其与所引用的VecRef<'a>具有完全相同的生命周期。

我将可变引用的生命周期与VecRef<'a>中借用的数据相关联:

fn create<'a>(r: &'a mut VecRef<'a>) {
    VecRefRef(r);
}
现在它起作用了。但是为什么呢?我怎么能提供这样的引用?在create()函数内部的可变引用r具有VecRef<'a>的生命周期,而不是'a'。为什么问题没有推到函数create()的调用方?
我注意到另一件我不理解的事情。如果我在VecRefRef<'a>结构中使用不可变引用,则当使用不同于'a'的生命周期的引用时,它似乎不再重要:
struct VecRef<'a>(&'a Vec<u8>);

struct VecRefRef<'a>(&'a VecRef<'a>); // now an immutable reference

fn main() {
    let v = vec![8u8, 9, 10];
    let mut ref_v = VecRef(&v);
    create(&mut ref_v);
}

fn create<'b, 'a>(r: &'b mut VecRef<'a>) {
    VecRefRef(r);
}

示例代码

这个示例可以正常运行,而第一个示例不行,因为 VecRefRef<'a> 拿到的是一个可变引用 VecRef<'a>。我知道可变引用有不同的别名规则(根本没有别名),但这与这里链接的生命周期有什么关系呢?

2个回答

26

警告:我所说的专业水平并不是很高。鉴于本文的长度,我可能会犯很多错误。

TL;DR:顶层值的生命周期是协变的。引用值的生命周期是不变的。

问题介绍

您可以通过将VecRef<'a>替换为&'a mut T来大大简化示例。

此外,应该删除main,因为更完整地讨论函数的一般行为比某些特定的生命周期实例化更好。

我们可以使用这个函数来代替VecRefRef的构造函数:

fn use_same_ref_ref<'c>(reference: &'c mut &'c mut ()) {}

在我们进一步探讨之前,了解 Rust 中生命周期如何隐式转换很重要。当将一个指针分配给另一个显式注释的名称时,会发生生命周期强制转换。最明显的是,这使得顶级指针的寿命缩短。因此,这不是一种典型的操作。

附言:我说“显式注释”,因为在隐式情况下,例如 let x = yfn f<T>(_: T) {},重新借用似乎不会发生。这是否有意是不清楚的。

完整的例子如下:

fn use_same_ref_ref<'c>(reference: &'c mut &'c mut ()) {}

fn use_ref_ref<'a, 'b>(reference: &'a mut &'b mut ()) {
    use_same_ref_ref(reference);
}

给出相同的错误:

error[E0623]: lifetime mismatch
 --> src/main.rs:5:26
  |
4 |     fn use_ref_ref<'a, 'b>(reference: &'a mut &'b mut ()) {
  |                                       ------------------
  |                                       |
  |                                       these two types are declared with different lifetimes...
5 |         use_same_ref_ref(reference);
  |                          ^^^^^^^^^ ...but data from `reference` flows into `reference` here

一个微不足道的修复

可以通过执行以下操作来修复它

fn use_same_ref_ref<'c>(reference: &'c mut &'c mut ()) {}

fn use_ref_ref<'a>(reference: &'a mut &'a mut ()) {
    use_same_ref_ref(reference);
}

由于签名现在在逻辑上是相同的。然而,不明显的是为什么。

let mut val = ();
let mut reference = &mut val;
let ref_ref = &mut reference;

use_ref_ref(ref_ref);

能够生成一个&'a mut &'a mut ()

一个不那么琐碎的修复方法

可以强制使用'a: 'b来代替。

fn use_same_ref_ref<'c>(reference: &'c mut &'c mut ()) {}

fn use_ref_ref<'a: 'b, 'b>(reference: &'a mut &'b mut ()) {
    use_same_ref_ref(reference);
}

这意味着外部引用的生命周期至少与内部引用的生命周期一样长。

这并不明显。

  • 为什么&'a mut &'b mut ()不能转换为&'c mut &'c mut (),或者

  • 是否比&'a mut &'a mut ()更好。

我希望回答这些问题。

无法解决的问题

断言'b: 'a无法解决问题。

fn use_same_ref_ref<'c>(reference: &'c mut &'c mut ()) {}

fn use_ref_ref<'a, 'b: 'a>(reference: &'a mut &'b mut ()) {
    use_same_ref_ref(reference);
}

另一个更令人惊讶的解决方案

将外部引用设为不可变可以解决问题。

fn use_same_ref_ref<'c>(reference: &'c &'c mut ()) {}

fn use_ref_ref<'a, 'b>(reference: &'a &'b mut ()) {
    use_same_ref_ref(reference);
}

甚至更令人惊讶的非修复方法!

使内部引用不可变根本没有帮助!

fn use_same_ref_ref<'c>(reference: &'c mut &'c ()) {}

fn use_ref_ref<'a, 'b>(reference: &'a mut &'b ()) {
    use_same_ref_ref(reference);
}

但是为什么?!

原因是...

等一下,首先我们来介绍方差

在计算机科学中,两个非常重要的概念是协变性逆变性。我不会使用这些名称(我会非常明确地说明我将如何转换),但是这些名称对于在互联网上搜索仍然非常有用。

在您理解这里的行为之前,了解方差的概念非常重要。如果您已经学过覆盖此内容的大学课程,或者您可以从其他上下文中记得它,那么您处于一个很好的位置。不过,将该想法与生命周期相关联的帮助可能仍然很有用。

简单情况-普通指针

考虑一个带有指针的堆栈位置:

    ║ Name      │ Type                │ Value
 ───╫───────────┼─────────────────────┼───────
  1 ║ val       │ i32                 │ -1
 ───╫───────────┼─────────────────────┼───────
  2 ║ reference │ &'x mut i32         │ 0x1

堆栈向下增长,因此 reference 栈位置是在 val 之后创建的,并且将在 val 之前被删除。
考虑你这样做。
let new_ref = reference;

获取

    ║ Name      │ Type        │ Value  
 ───╫───────────┼─────────────┼─────── 
  1 ║ val       │ i32         │ -1     
 ───╫───────────┼─────────────┼─────── 
  2 ║ reference │ &'x mut i32 │ 0x1    
 ───╫───────────┼─────────────┼─────── 
  3 ║ new_ref   │ &'y mut i32 │ 0x1    

'y'的有效生命周期是什么?

考虑两个可变指针操作:

  • 读取
  • 写入

读取 防止'y'增长,因为'x'引用只保证对象在'x'的范围内存活。但是,读取不能防止'y'缩小,因为在指向的值存活期间进行的任何读取都会得到一个独立于生命周期'y'的值。

写入也防止'y'增长,因为无法写入失效的指针。但是,写入不能防止'y'缩小,因为对指针的任何写入都会将值复制进去,这将使它独立于生命周期'y'

难点-指针指针

考虑一些带有指针指针的堆栈位置:

    ║ Name      │ Type                │ Value  
 ───╫───────────┼─────────────────────┼─────── 
  1 ║ val       │ i32                 │ -1     
 ───╫───────────┼─────────────────────┼─────── 
  2 ║ reference │ &'a mut i32         │ 0x1    
 ───╫───────────┼─────────────────────┼─────── 
  3 ║ ref_ref   │ &'x mut &'a mut i32 │ 0x2    

考虑你所做的事情。
let new_ref_ref = ref_ref;

获取

    ║ Name        │ Type                │ Value  
 ───╫─────────────┼─────────────────────┼─────── 
  1 ║ val         │ i32                 │ -1     
 ───╫─────────────┼─────────────────────┼─────── 
  2 ║ reference   │ &'a mut i32         │ 0x1    
 ───╫─────────────┼─────────────────────┼─────── 
  3 ║ ref_ref     │ &'x mut &'a mut i32 │ 0x2    
 ───╫─────────────┼─────────────────────┼─────── 
  4 ║ new_ref_ref │ &'y mut &'b mut i32 │ 0x2    

现在有两个问题:
1. 'y 的有效生命周期是什么? 2. 'b 的有效生命周期是什么?
让我们先考虑使用两个可变指针操作的 'y
- 读取 - 写入
读取会阻止 'y 增长,因为 'x 引用仅保证对象在 'x 作用域内保持活动状态。但是,读取不会阻止 'y 收缩,因为在指向的值存在期间进行的任何读取都会导致与生命周期 'y 无关的值。
写入也会阻止 'y 增长,因为不能写入无效的指针。但是,写入不会阻止 'y 收缩,因为对指针的任何写入都会将其复制到值中,从而使其独立于生命周期 'y
这与以前相同。

现在考虑具有两个可变指针操作的'b

读取会防止'b增长,因为如果从外部指针中提取内部指针,则可以在'a过期后读取它。

写入也会防止'b增长,因为如果从外部指针中提取内部指针,则可以在'a过期后向其写入。

读取写入一起也会防止'b收缩,因为存在以下情况:

let ref_ref: &'x mut &'a mut i32 = ...;

{
    // Has lifetime 'b, which is smaller than 'a
    let new_val: i32 = 123;

    // Shrink 'a to 'b
    let new_ref_ref: &'x mut &'b mut i32 = ref_ref;

    *new_ref_ref = &mut new_val;
}

// new_ref_ref is out of scope, so ref_ref is usable again
let ref_ref: &'a mut i32 = *ref_ref;
// Oops, we have an &'a mut i32 pointer to a dropped value!
因此,'b 无法从 'a 收缩或增长,因此 'a == 'b 正好。 这意味着 &'y mut &'b mut i32 在生命周期 'b 中是不变的。

好的,这解决了我们的问题吗?

还记得代码吗?

fn use_same_ref_ref<'c>(reference: &'c mut &'c mut ()) {}

fn use_ref_ref<'a, 'b>(reference: &'a mut &'b mut ()) {
    use_same_ref_ref(reference);
}

当你调用use_same_ref_ref时,会尝试进行转换。
&'a mut &'b mut ()  →  &'c mut &'c mut ()

现在请注意,由于我们对方差的讨论,'b == 'c。因此,实际上我们正在进行强制类型转换。
&'a mut &'b mut ()  →  &'b mut &'b mut ()

外部的&'a只能被缩小。为了做到这一点,编译器需要知道。
'a: 'b

编译器不知道这个,所以编译失败。
我们的其他例子呢?
第一个是:
fn use_same_ref_ref<'c>(reference: &'c mut &'c mut ()) {}

fn use_ref_ref<'a>(reference: &'a mut &'a mut ()) {
    use_same_ref_ref(reference);
}

现在编译器需要 'a: 'a,而不是 'a: 'b,这是显然成立的。

第二个直接断言了 'a: 'b

fn use_same_ref_ref<'c>(reference: &'c mut &'c mut ()) {}

fn use_ref_ref<'a: 'b, 'b>(reference: &'a mut &'b mut ()) {
    use_same_ref_ref(reference);
}

第三个断言'b: 'a

fn use_same_ref_ref<'c>(reference: &'c mut &'c mut ()) {}

fn use_ref_ref<'a, 'b: 'a>(reference: &'a mut &'b mut ()) {
    use_same_ref_ref(reference);
}

这不起作用,因为这不是所需的断言。

不可变性怎么样?

我们在这里有两种情况。第一种是使外部引用不可变。

fn use_same_ref_ref<'c>(reference: &'c &'c mut ()) {}

fn use_ref_ref<'a, 'b>(reference: &'a &'b mut ()) {
    use_same_ref_ref(reference);
}

这个有效。为什么?
嗯,考虑我们之前缩小 &'b 的问题:

Read and write together also prevent 'b from shrinking, because of this scenario:

let ref_ref: &'x mut &'a mut i32 = ...;

{
    // Has lifetime 'b, which is smaller than 'a
    let new_val: i32 = 123;

    // Shrink 'a to 'b
    let new_ref_ref: &'x mut &'b mut i32 = ref_ref;

    *new_ref_ref = &mut new_val;
}

// new_ref_ref is out of scope, so ref_ref is usable again
let ref_ref: &'a mut i32 = *ref_ref;
// Oops, we have an &'a mut i32 pointer to a dropped value!

Ergo, 'b cannot shrink and it cannot grow from 'a, so 'a == 'b exactly.

这只有在我们能够将内部引用替换为一些新的、不够长寿的引用时才会发生。如果我们无法交换引用,这就不是一个问题。因此,缩短内部引用的寿命是可能的。

那失败的呢?

使内部引用不可变并不能解决问题:

fn use_same_ref_ref<'c>(reference: &'c mut &'c ()) {}

fn use_ref_ref<'a, 'b>(reference: &'a mut &'b ()) {
    use_same_ref_ref(reference);
}

考虑到之前提到的问题从未涉及对内部引用的任何读取,这就有意义了。事实上,以下是修改后的有问题代码以进行演示:
let ref_ref: &'x mut &'a i32 = ...;

{
    // Has lifetime 'b, which is smaller than 'a
    let new_val: i32 = 123;

    // Shrink 'a to 'b
    let new_ref_ref: &'x mut &'b i32 = ref_ref;

    *new_ref_ref = &new_val;
}

// new_ref_ref is out of scope, so ref_ref is usable again
let ref_ref: &'a i32 = *ref_ref;
// Oops, we have an &'a i32 pointer to a dropped value!

还有一个问题

已经过了相当长的时间,但是回想一下:

One can instead enforce 'a: 'b

fn use_same_ref_ref<'c>(reference: &'c mut &'c mut ()) {}

fn use_ref_ref<'a: 'b, 'b>(reference: &'a mut &'b mut ()) {
    use_same_ref_ref(reference);
}

This means that the lifetime of the outer reference is at least as large as the lifetime of the inner one.

It's not obvious

  • why &'a mut &'b mut () is not castable to &'c mut &'c mut (), or

  • whether this is better than &'a mut &'a mut ().

I hope to answer these questions.

我们已经回答了第一个问题,但第二个问题呢?'a: 'b是否允许比'a == 'b更多的东西?
考虑一些类型为&'x mut &'y mut ()的调用者。如果'x:'y,那么它会自动转换为&'y mut &'y mut ()。相反,如果'x == 'y,那么'x:'y已经成立!因此,唯一重要的区别在于,如果您希望向调用者返回包含'x的类型,则只有调用者能够区分两者之间的区别。由于这里并非如此,所以这两者是等价的。
还有一件事
如果您写:
let mut val = ();
let mut reference = &mut val;
let ref_ref = &mut reference;

use_ref_ref(ref_ref);

其中定义了use_ref_ref

fn use_ref_ref<'a: 'b, 'b>(reference: &'a mut &'b mut ()) {
    use_same_ref_ref(reference);
}

我该如何将代码强制执行'a: 'b?从检查上看,似乎相反的情况是正确的!

嗯,记住

let reference = &mut val;

由于此时它是外部生命周期,因此可以缩短其生命周期。因此,即使指针超出该生命周期,它也可以引用比val的实际生命周期更小的生命周期!


“Read” 防止 'b 增长,因为如果从外部指针中提取内部指针,则在 'a 过期后仍能读取它。-- 你能详细说明一下吗? - soupybionics
@soupybionics 我怀疑我错了。让我回复你。 - Veedrac
@soupybionics 抱歉,我好像忘记了你的问题。我记得当时研究过它并认为我错了,但我不记得细节了。我会注意到 &'static &'b 可以 转换为 &'static &'static,这表明你是对的,但我认为这不正确。相反,可能存在一个隐含的假设,即 'b:'a;将 &'a &'b 转换为 &'a &'static 会失败。 - Veedrac

7

create()函数中,可变引用r的生命周期为VecRef<'a>,而不是'a

这是一个常见的困惑点。请查看此函数定义:

fn identity<'a, T>(val: &'a T) -> &'a T { val }

在函数定义中,'a 是一个 通用的 生命周期参数,类似于一个通用类型参数(T)。当函数被调用时,调用者决定'aT 的具体值。让我们回顾一下你的main函数:
fn main() {
    let v = vec![8u8, 9, 10];   // 1 |-lifetime of `v`
    let mut ref_v = VecRef(&v); // 2 |  |-lifetime of `ref_v` 
    create(&mut ref_v);         // 3 |  |
}
v将在整个main(1-3)的运行中都存在,但是ref_v仅在最后两条语句(2-3)中存在。请注意,ref_v引用的是一个比它自己存在时间更长的值。如果你再取一个对ref_v的引用,你就得到了一个对存活于(2-3)的某个东西的引用,而这个东西本身则具有对存活于(1-3)的某个东西的引用。
看看你修复后的方法:
fn create<'a>(r: &'a mut VecRef<'a>)

这意味着在此函数调用中,对于 VecRef 的引用和它所包含的引用必须是相同的。可以选择一个满足这个条件的生命周期 — (2-3)。
请注意,您的结构定义目前要求这两个生命周期相同。您可以允许它们不同:
struct VecRefRef<'a, 'b: 'a>(&'a mut VecRef<'b>);
fn create<'a, 'b>(r: &'a mut VecRef<'b>)

请注意,您必须使用语法'b: 'a来表示生命周期'b将超过'a
如果我使用不可变引用[...],那就不再重要了。
这一点我不太确定。我认为发生的事情是,因为你有一个不可变的借用,编译器可以自动为你在更小的范围内重新借用。这使得生命周期匹配。正如您指出的那样,可变引用不能有任何别名,甚至是更小的范围,因此编译器在这种情况下无法提供帮助。

我明白了。在 create 函数的主体内,选定的 lifetime 将成为2-3的作用域,因为它是唯一与约束匹配的提供的具体 lifetimes 集合中的一个。因此,它将是创建的 VecRefRef<'a> 的 lifetime 'a。 - jtepe
请注意,您必须使用语法'b:'a来表示生命周期'a将超过'b的生命周期。难道不应该是“...表示生命周期'b将超过'a的生命周期。”吗? - soupybionics

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接