即使存在“&mut T”,我是否可以将生命周期参数强制缩短(合理地)?

Can I coerce a lifetime parameter to a shorter lifetime (soundly) even in the presence of `&mut T`?

我正在尝试用 Rust 中的父指针创建一棵树。节点结构上的一种方法给我带来了生命周期问题。这是一个最小的例子,明确地写了生命周期以便我可以理解它们:

use core::mem::transmute;

pub struct LogNode<'n>(Option<&'n mut LogNode<'n>>);

impl<'n> LogNode<'n> {
    pub fn child<'a>(self: &'a mut LogNode<'n>) -> LogNode<'a> {
        LogNode(Some(self))
    }

    pub fn transmuted_child<'a>(self: &'a mut LogNode<'n>) -> LogNode<'a> {
        unsafe {
            LogNode(Some(
                transmute::<&'a mut LogNode<'n>, &'a mut LogNode<'a>>(self)
            ))
        }
    }
}

(Playground link)

Rust 抱怨 child...

error[E0495]: cannot infer an appropriate lifetime for lifetime parameter 'n due to conflicting requirements

...但是 transmuted_child 没问题。

我想我明白为什么 child 不会编译:self 参数的类型是 &'a mut LogNode<'n> 但子节点包含 &'a mut LogNode<'a>,而 Rust 不我不想强迫 LogNode<'n>LogNode<'a>。如果我将可变引用更改为共享引用,it compiles fine,那么听起来可变引用是一个问题,特别是因为 &mut TT 上是不变的(而 &T 是协变的).我猜 LogNode 中的可变引用冒泡使 LogNode 本身在其生命周期参数内不变。

但我不明白为什么这是真的——直觉上,将 LogNode<'n> 变成 LogNode<'a> 来缩短其内容的生命周期是非常合理的。由于生命周期不会变长,因此在其生命周期之后无法访问任何值,而且我想不出任何其他可能发生的不良行为。

transmuted_child 避免了生命周期问题,因为它避开了借用检查器,但我不知道使用不安全的 Rust 是否合理,即使是,我更愿意使用安全的 Rust如果可能的话。我可以吗?

这个问题我能想到三个可能的答案:

  1. child 可以完全用安全的 Rust 实现,方法如下。
  2. child 不能完全用安全的 Rust 实现,但 transmuted_child 是合理的。
  3. child 不能完全用安全的 Rust 实现,transmuted_child 是不可靠的。

编辑 1:修复了 &mut T 在引用的生命周期内不变的声明。 (没有正确阅读 nomicon。)

编辑 2:修复了我的第一个编辑摘要。

答案是 #3:child 不能在安全的 Rust 中实现,transmuted_child 是不可靠的¹。这是一个使用 transmuted_child(没有其他 unsafe 代码)导致段错误的程序:

fn oops(arg: &mut LogNode<'static>) {
    let mut short = LogNode(None);
    let mut child = arg.transmuted_child();
    if let Some(ref mut arg) = child.0 {
        arg.0 = Some(&mut short);
    }
}

fn main() {
    let mut node = LogNode(None);
    oops(&mut node);
    println!("{:?}", node);
}

short是一个短命的局部变量,但是由于你可以使用transmuted_child来缩短LogNode的生命周期参数,你可以塞入一个对[=15的引用=] 在 LogNode 中应该是 'static。当 oops returns 时,引用不再有效,并且尝试访问它会导致未定义的行为(对我来说是段错误)。


¹ 这有一些微妙之处。确实 transmuted_child 本身 没有未定义的行为,但是因为它使其他代码如 oops 成为可能,调用或暴露它可能会使你的界面不健全.要将此函数公开为安全 API 的一部分,您必须非常小心,不要公开其他会让用户编写类似 oops 的功能。如果你不能这样做,并且你不能避免写 transmuted_child,那么应该写成 unsafe fn.

要理解为什么不可变版本有效而​​可变版本不健全(如所写),我们必须讨论 subtyping and variance

Rust 大多没有子类型。值通常具有唯一类型。然而,Rust 确实 具有子类型的一个地方是生命周期。如果 'a: 'b(读取 'a'b 长),那么,例如 &'a T&'b T 的子类型,直观上是因为可以处理更长的生命周期好像他们更短。

方差是子类型传播的方式。如果 AB 的子类型,并且我们有一个泛型类型 Foo<T>,那么 Foo<A> 可能是 Foo<B> 的子类型,反之亦然,或者两者都不是。在第一种情况下,子类型的方向保持不变,Foo<T> 相对于 T 是协变的。第二种情况,方向相反,称为逆变,第三种情况,称为不变。

对于这种情况,相关类型是 &'a T&'a mut T。两者在 'a 中都是协变的(因此可以将具有较长生命周期的引用强制转换为具有较短生命周期的引用)。 &'a TT 中是协变的,但 &'a mut TT.

中是 不变的

Nomicon(上面链接)中解释了这样做的原因,所以我将只向您展示那里给出的(稍微简化的)示例。 Trentcl 的代码是如果 &'a mut TT.

中协变会出现什么问题的工作示例
fn evil_feeder(pet: &mut Animal) {
    let spike: Dog = ...;

    // `pet` is an Animal, and Dog is a subtype of Animal,
    // so this should be fine, right..?
    *pet = spike;
}

fn main() {
    let mut mr_snuggles: Cat = ...;
    evil_feeder(&mut mr_snuggles);  // Replaces mr_snuggles with a Dog
    mr_snuggles.meow();             // OH NO, MEOWING DOG!
}

那么为什么 child 的不可变版本有效,而可变版本无效?在不可变版本中,LogNode 包含对 LogNode 的不可变引用,因此通过生命周期和类型参数的协变,LogNode 在其生命周期参数中是协变的。如果 'a: 'b,则 LogNode<'a>LogNode<'b> 的子类型。

我们有 self: &'a LogNode<'n>,这意味着 'n: 'a(否则这个借用将比 LogNode<'n> 中的数据更持久)。因此,由于 LogNode 是协变的,因此 LogNode<'n>LogNode<'a> 的子类型。此外,不可变引用中的协变再次允许 &'a LogNode<'n> 成为 &'a LogNode<'a> 的子类型。因此,对于 child.

中的 return 类型,可以根据需要将 self: &'a LogNode<'n> 强制为 &'a LogNode<'a>

对于可变版本,LogNode<'n>'n 中不是协变的。这里的方差归结为 &'n mut LogNode<'n> 的方差。但是由于此处可变引用的“T”部分存在生命周期,因此可变引用的不变性(在 T 中)意味着这也必须是不变的。

这一切结合起来表明 self: &'a mut LogNode<'n> 不能被强制转换为 &'a mut LogNode<'a>。所以该函数无法编译。


一个解决方案是添加生命周期限制 'a: 'n,尽管如上所述,我们已经有了 'n: 'a,因此这会强制两个生命周期相等。这可能适用于您的其余代码,也可能不适用于它,因此请对它持保留态度。