我正在尝试用 Rust 制作一棵带有父指针的树。节点结构上的方法给我带来了生命周期问题。这是一个最小的例子,明确地写出了生命周期,以便我可以理解它们:
\n\nuse 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\nRust 抱怨child
...
\n\n\n\n
\'n
错误[E0495]:由于要求冲突,无法推断生命周期参数的适当生命周期
...但是没关系transmuted_child
。
我想我明白为什么child
无法编译:self
参数的类型是,&\'a mut LogNode<\'n>
但子节点包含一个&\'a mut LogNode<\'a>
,并且 Rust 不想强制LogNode<\'n>
为LogNode<\'a>
。如果我将可变引用更改为共享引用,它会编译良好,所以听起来可变引用是一个问题,特别是因为&mut T
是 不变的T
(而&T
是 协变的)。我猜想中的可变引用会LogNode
冒泡,使其LogNode
在其生命周期参数内保持不变。
但我不明白为什么这是真的\xe2\x80\x94直观上感觉通过将LogNode<\'n>
其转换为LogNode<\'a>
. 由于生命周期不会变得更长,因此无法访问超过其生命周期的值,并且我无法想到可能发生的任何其他不合理行为。
transmuted_child
避免了生命周期问题,因为它避开了借用检查器,但我不知道使用不安全的 Rust 是否合理,即使是,如果可能的话,我更愿意使用安全的 Rust。我可以吗?
我可以想到这个问题的三个可能的答案:
\n\nchild
可以完全用安全的 Rust 来实现,具体方法如下。child
不能完全用安全的 Rust 实现,但transmuted_child
很健全。child
不能完全在安全的 Rust 中实现,并且transmuted_child
是不健全的。&mut T
编辑 1:修复了在引用的生命周期内不变的声明。(没有正确阅读指南。)
编辑 2:修复了我的第一次编辑摘要。
\n答案是#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
'static
oops
1 这其中有一些微妙之处。确实,transmuted_child
它本身没有未定义的行为,但因为它使其他代码成为oops
可能,调用或暴露它可能会使你的界面不健全。要将此函数公开为安全 API 的一部分,您必须非常小心,不要公开其他功能,以免用户编写类似oops
. 如果你做不到这一点,并且你无法避免写作transmuted_child
,那么它应该成为一个unsafe fn
.
为了理解为什么不可变版本有效而可变版本不健全(如所写),我们必须讨论子类型和方差。
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 T
is 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
,所以这迫使两个生命周期相等。这可能适用于您的其余代码,也可能不适用于您的代码,因此请对此持保留态度。