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

ash*_*eoi 4 lifetime rust

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

\n\n
use core::mem::transmute;\n\npub struct LogNode<\'n>(Option<&\'n mut LogNode<\'n>>);\n\nimpl<\'n> LogNode<\'n> {\n    pub fn child<\'a>(self: &\'a mut LogNode<\'n>) -> LogNode<\'a> {\n        LogNode(Some(self))\n    }\n\n    pub fn transmuted_child<\'a>(self: &\'a mut LogNode<\'n>) -> LogNode<\'a> {\n        unsafe {\n            LogNode(Some(\n                transmute::<&\'a mut LogNode<\'n>, &\'a mut LogNode<\'a>>(self)\n            ))\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

游乐场链接

\n\n

Rust 抱怨child...

\n\n
\n

\'n错误[E0495]:由于要求冲突,无法推断生命周期参数的适当生命周期

\n
\n\n

...但是没关系transmuted_child

\n\n

我想我明白为什么child无法编译:self参数的类型是,&\'a mut LogNode<\'n>但子节点包含一个&\'a mut LogNode<\'a>,并且 Rust 不想强制LogNode<\'n>LogNode<\'a>。如果我将可变引用更改为共享引用,它会编译良好,所以听起来可变引用是一个问题,特别是因为&mut T是 不变的T(而&T是 协变的)。我猜想中的可变引用会LogNode冒泡,使其LogNode在其生命周期参数内保持不变。

\n\n

但我不明白为什么这是真的\xe2\x80\x94直观上感觉通过将LogNode<\'n>其转换为LogNode<\'a>. 由于生命周期不会变得更长,因此无法访问超过其生命周期的值,并且我无法想到可能发生的任何其他不合理行为。

\n\n

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

\n\n

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

\n\n
    \n
  1. child可以完全用安全的 Rust 来实现,具体方法如下。
  2. \n
  3. child不能完全用安全的 Rust 实现,但transmuted_child很健全。
  4. \n
  5. child不能完全在安全的 Rust 中实现,并且transmuted_child是不健全的。
  6. \n
\n\n

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

\n\n

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

\n

tre*_*tcl 5

答案是#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);
}
Run Code Online (Sandbox Code Playgroud)

short是一个短期局部变量,但由于您可以使用transmuted_child来缩短 的生存期参数,因此您可以将对应该是 的LogNode引用填充到short内部。返回时,引用不再有效,并且尝试访问它会导致未定义的行为(对我来说是段错误)。LogNode'staticoops


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


SCa*_*lla 5

为了理解为什么不可变版本有效而​​可变版本不健全(如所写),我们必须讨论子类型和方差

Rust 大多没有子类型。值通常具有唯一的类型。然而,Rust确实有子类型的地方之一是生命周期。例如,如果'a: 'b(read'a长于'b),则&'a T是 的子类型&'b T,直观上是因为较长的生命周期可以被视为较短的生命周期。

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

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

其原因在 Nomicon(上面链接)中进行了解释,因此我将向您展示那里给出的(稍微简化的)示例。Trentcl 的代码是一个工作示例,说明了 if &'a mut Tis covariant in T.

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!
}
Run Code Online (Sandbox Code Playgroud)

那么为什么不可变版本可以child工作,而可变版本却不行呢?在不可变版本中,LogNode包含对 a 的不可变引用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>。因此,self: &'a LogNode<'n>可以&'a LogNode<'a>根据需要强制转换为 中的返回类型child

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

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


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