在Rust中,生命周期的子类型关系很难理解。

32

我觉得自己很傻,因为我已经多次浏览了Rust文档中的标记部分以及关于子类型变异性的维基百科文章,但对生命周期子类型关系的理解并没有提高。

我想我只是习惯了“典型的面向对象编程风格”中的子类型关系,例如“Cat <: Animal”表示“Cat是Animal的一个子类型”,其中“S是T的子类型”意味着“任何S项都可以安全地用于期望T类型的上下文中”。到目前为止还好。

但是这如何适用于生命周期? Rust中现在定义的方式似乎是(*)

(#1) 'a <: 'b <=> 生命周期a不超过生命周期b。

你可能会认为“当然是这个意思!”可能是因为<:看起来类似于小于运算符,也可能是因为“sub”让你想到子集,而短寿命肯定是长寿命的子集。但是如果'a不比'b长,'a真的是'b的子类型吗?让我们尝试应用维基百科对子类型关系的定义:
(#2) 'a <: 'b <=> 生命周期a可以安全地用于期望生命周期b的上下文中。
我的问题是我无法协调这一点。你如何从#2到#1?因为对我来说,这似乎是一个矛盾...如果你希望某些东西至少活到b,并且你有一个寿命为a且比b短的东西,显然不能在需要寿命为b的上下文中使用它,对吧?是我自己的问题还是我们对生命周期的子类型关系理解错了呢?
编辑:(*)根据IRC频道中的Ms2ger所说,情况确实如此。它也符合用于Items迭代器中的逆变寿命标记的文档。
编辑2:ContravariantLifetime和CovariantLifetime标记已被移除。现在我们有PhantomData作为marker模块的替代品。

7
Rust中有关生命周期的概念是受到了另一种编程语言Cyclone的区域(region)概念的启发。参见《Cyclone中基于区域的内存管理(Region-Based Memory Management in Cyclone)》第2.3节中关于区域子类型(region subtyping)的讨论,可能会有所帮助!我相信 a<:b 等价于 b 的生命周期不长于 a 的生命周期。 - mwhittaker
4
我认为这个讨论可能会有帮助。 - Vladimir Matveev
3
如果你还记得的话,那就是 Rust 中定义生命周期子类型的相反方式。 - sellibitze
2
@sellibitze,我认为你是对的!Rust和Cyclone中子类型的定义似乎是相反的。这里是我之前链接的论文摘录,它明确了Cyclone的子类型规则:“我们观察到,如果与p1对应的区域比与p2对应的区域存在时间更长,那么在我们期望使用*p2类型的值时,使用*p1类型的值是安全的。” - mwhittaker
1
与 GitHub 上链接问题的解决方案是:“生命周期不是类型,因此没有子类型顺序;但是,它们具有由区域包含给出的排序。” - Potatoswatter
显示剩余2条评论
1个回答

7
免责声明:我不是一个计算机科学专家,所以本答案将专注于实际概念,我甚至不会尝试将其与理论概念联系起来,以免把事情搞糟。
我认为问题在于试图将子类型概念应用于不是类型的东西。
'a是寿命
&'a T是一种类型
您可以比较&'a T和&'b U,看它们是否遵守子类型关系,但您不能在抽象中建立两个生命周期之间的子类型关系,因为:
有时,为了可替换,新的寿命必须大于被替换的寿命。
有时,为了可替换,新的寿命必须小于被替换的寿命。
我们可以通过两个简单的例子来检查这一点。
第一个例子可能是最简单的:如果寿命更长,则可以替换寿命!
//  Using a lifetime as a bound
struct Reference<'a, T>
    where T: 'a
{
    data: &'a T
}

fn switch<'a, 'b, T>(r: &mut Reference<'a, T>, new: &'b T)
    where 'b: 'a
{
    r.data = new;
}

在这里,编译器只允许替换如果'b至少与'a相同或更大,这由生命周期限制'b:'a表示。这是因为Rust避免悬垂引用,因此容器只能包含对将超出它的对象的引用。
当用作保证时,较长的生命周期是较短的生命周期的子类型,可以替换它的位置。正如@aturon所提到的,这暗示在这种用法中,'static是所有生命周期的子类型。
第二个例子有点棘手:如果寿命更短,则可以替换生命周期!
让我们从以下内容开始:
struct Token;

fn restrict<'a, 'b, T>(original: &'a T, _: &'b Token) -> &'b T
    where 'a: 'b
{
    original
}

以下用法是正确的:
fn main() {
    let i = 4;

    {
        let lesser = Token;
        let k = restrict(&i, &lesser);
        println!("{}", k);
    }
}

我们之前的演示说过,我们可以将一个更长的生命周期代替一个较短的生命周期:

fn main() {
    let greater = Token;
    let j;  // prevent unification of lifetimes

    {
        let i = 4;
        j = restrict(&i, &greater);
    }
    println!("{}", j);
}

error: `i` does not live long enough
j = restrict(&i, &greater);

当作为约束条件时,较短的生命周期是较长生命周期的子类型,并且可以替代它。在这种情况下,'static是所有生命周期的超类型。
因此,生命周期之间不存在单一的子类型关系,因为它们具有两个根本上不同的用途!
总结一下:
- 当用作保证时:greater <: lesser - 当用作约束条件时:lesser <: greater 注意:某些生命周期可能同时作为保证和约束条件。

1
我猜这里的关键词是协变性(如果 F 是协变的,那么 a <: b 意味着 F(a) <: F(b))和逆变性(如果 F 是逆变的,a <: b 意味着 F(b) <: F(a))。 - user824425
1
@Rhymoid:谢谢!(我总是记不住哪个是哪个...) - Matthieu M.

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