在我偶然发现下面的代码之前,我确信类型的生命周期参数中的生命周期总是比其自己的实例更长。换句话说,给定foo: Foo<'a>
,'a
总是比foo
更长。然后我被@Luc Danton(Playground)介绍了这个反辩论代码:
#[derive(Debug)]
struct Foo<'a>(std::marker::PhantomData<fn(&'a ())>);
fn hint<'a, Arg>(_: &'a Arg) -> Foo<'a> {
Foo(std::marker::PhantomData)
}
fn check<'a>(_: &Foo<'a>, _: &'a ()) {}
fn main() {
let outlived = ();
let foo;
{
let shortlived = ();
foo = hint(&shortlived);
// error: `shortlived` does not live long enough
//check(&foo, &shortlived);
}
check(&foo, &outlived);
}
即使由foo
创建的hint
似乎认为生命周期与其自身无关,并且对它的引用会传递给更广泛范围内的函数,代码完全按照原样编译。取消注释代码中声明的行会触发编译错误。或者,将Foo
更改为struct tuple (PhantomData<&'a ()>)
也会导致代码不再编译时出现相同类型的错误(Playground)。
Rust代码有效吗?这里编译器的原因是什么?
答案 0 :(得分:43)
尽管您的意图很好,但您的hint
功能可能没有您期望的效果。但是,在我们了解正在发生的事情之前,我们还有很多理由可以解决。
让我们从这开始:
fn ensure_equal<'z>(a: &'z (), b: &'z ()) {}
fn main() {
let a = ();
let b = ();
ensure_equal(&a, &b);
}
好的,所以在main
中,我们定义了两个变量a
和b
。由于被不同的let
语句引入,它们具有不同的生命周期。 ensure_equal
需要两个相同生命周期的引用。然而,这段代码编译。为什么呢?
这是因为,'a: 'b
(阅读:'a
超过'b
),&'a T
是&'b T
a
我们假设'a
的生命周期为b
,'b
的生命周期为'a: 'b
。这是a
的事实,因为ensure_equal
是先引入的。在调用&'a ()
时,参数分别为&'b ()
和'a
,分别为 1 。这里的类型不匹配,因为'b
和&'a ()
的生命周期不同。但是编译器还没有放弃!它知道&'b ()
是&'a ()
的子类型。换句话说,&'b ()
是 &a
。因此,编译器将强制表达式&'b ()
键入&'b ()
,以便两个参数都键入&'a ()
。这解决了类型不匹配问题。
如果您对&#34; subtypes&#34;的应用感到困惑。有生命周期,那么让我用Java术语来重述这个例子。我们将Programmer
替换为&'b ()
,将Person
替换为Programmer
。现在让我们说Person
来自Programmer
:Person
因此是Programmer
的子类型。这意味着我们可以获取类型为Person
的变量,并将其作为参数传递给期望类型为T
的参数的函数。这就是为什么以下代码将成功编译的原因:编译器将Person
的{{1}}解析为main
中的调用。
class Person {}
class Programmer extends Person {}
class Main {
private static <T> void ensureSameType(T a, T b) {}
public static void main(String[] args) {
Programmer a = null;
Person b = null;
ensureSameType(a, b);
}
}
这种子类型关系的非直观方面可能是寿命越长是寿命越短的子类型。但是可以这样想:在Java中,假装Programmer
是Person
是安全的,但你不能假设Person
是一个Programmer
Vec
。同样,假装变量的更短生命周期是安全的,但您不能假设具有某些已知生命周期的变量实际上具有更长的 >一生。毕竟,Rust的整个生命周期都是为了确保您不会在实际生命周期内访问对象。
现在,让我们谈谈subtype。那是什么?
Variance是类型构造函数关于其参数的属性。 Rust中的类型构造函数是具有未绑定参数的泛型类型。例如,
T
是一个类型构造函数,它接受Vec<T>
并返回&
。&mut
和Vec<T>
是带有两个输入的类型构造函数:生命周期和指向的类型。
通常情况下,您会期望&'a T
的所有元素都具有相同的类型(我们此处并未讨论特征对象)。但差异让我们为此作弊。
'a
协变超过T
和&'a T
。这意味着无论我们在类型参数中看到&'a T
,我们都可以用fn main() {
let a = ();
let b = ();
let v = vec![&a, &b];
}
的子类型替换它。让我们看看它是如何运作的:
a
我们已经确定b
和&a
的生命周期不同,&b
和Vec
这两个词的类型不同 1 。那么为什么我们可以用这些来&a
呢?推理与上述相同,因此我总结一下:&'b ()
被强制转换为v
,因此Vec<&'b ()>
的类型为fn(T)
。
fn(T)
是Rust的一个特例。 T
逆变超过Vec
。让我们构建一个fn foo(_: &'static ()) {}
fn bar<'a>(_: &'a ()) {}
fn quux<'a>() {
let v = vec![
foo as fn(&'static ()),
bar as fn(&'a ()),
];
}
fn main() {
quux();
}
函数!
v
这个编译。但quux
中Vec<fn(&'static ())>
的类型是什么?是Vec<fn(&'a ())>
还是fn foo(_: &'static ()) {}
fn bar<'a>(_: &'a ()) {}
fn quux<'a>(a: &'a ()) {
let v = vec![
foo as fn(&'static ()),
bar as fn(&'a ()),
];
v[0](a);
}
fn main() {
quux(&());
}
?
我会给你一个提示:
error[E0495]: cannot infer an appropriate lifetime due to conflicting requirements
--> <anon>:5:13
|
5 | let v = vec![
| _____________^ starting here...
6 | | foo as fn(&'static ()),
7 | | bar as fn(&'a ()),
8 | | ];
| |_____^ ...ending here
|
note: first, the lifetime cannot outlive the lifetime 'a as defined on the body at 4:23...
--> <anon>:4:24
|
4 | fn quux<'a>(a: &'a ()) {
| ________________________^ starting here...
5 | | let v = vec![
6 | | foo as fn(&'static ()),
7 | | bar as fn(&'a ()),
8 | | ];
9 | | v[0](a);
10| | }
| |_^ ...ending here
note: ...so that reference does not outlive borrowed content
--> <anon>:9:10
|
9 | v[0](a);
| ^
= note: but, the lifetime must be valid for the static lifetime...
note: ...so that types are compatible (expected fn(&()), found fn(&'static ()))
--> <anon>:5:13
|
5 | let v = vec![
| _____________^ starting here...
6 | | foo as fn(&'static ()),
7 | | bar as fn(&'a ()),
8 | | ];
| |_____^ ...ending here
= note: this error originates in a macro outside of the current crate
error: aborting due to previous error
这个没有编译。以下是编译器消息:
&'a ()
我们尝试使用v[0]
参数调用向量中的一个函数。但是&'static ()
需要'a
,并且不能保证'static
是v
,所以这是无效的。因此,我们可以得出结论Vec<fn(&'static ())>
的类型为hint
。正如您所看到的,逆变量与协方差相反:我们可以用更长的替换短寿命。
哇,现在回到你的问题。首先,让我们看看编译器对hint
的调用做了什么。 fn hint<'a, Arg>(_: &'a Arg) -> Foo<'a>
具有以下签名:
Foo
'a
逆变超过Foo
因为fn
包裹PhantomData
(或者更确切地说,假装,感谢fn(T)
,但是当我们谈论方差时,这并没有什么不同;两者都有相同的效果),T
是T
的逆变而&'a ()
这里是hint
。
当编译器尝试解析对shortlived
的调用时,它只会考虑hint
的生命周期。因此,Foo
会返回shortlived
的{{1}}生命周期。但是当我们尝试将其分配给变量foo
时,我们遇到了一个问题:类型上的生命周期参数总是比类型本身更长,而shortlived
的生命周期并不存在超过foo
的生命周期,显然,我们无法将此类型用于foo
。如果Foo
对'a
有协变性,那就是它的结束,你就会收到错误。但Foo
逆变超过'a
,因此我们可以用更大的生命周期替换shortlived
的生命周期。终生可以是任何超过foo
一生的寿命。请注意&#34;超过&#34;与&#34;严格超过&#34;:不同之处在于'a: 'a
('a
超过'a
)是真的,但'a
严格超过{{ 1}}是假的(即一生被认为比自己寿命长,但它并没有严格地超过本身)。因此,我们最终可能会'a
类型为foo
,其中Foo<'a>
恰好是'a
本身的生命周期。
现在让我们看foo
(第二个)。这个编译是因为check(&foo, &outlived);
被强制执行,因此缩短了生命周期以匹配&outlived
的生命周期。这是有效的,因为foo
的有效期比outlived
长,而foo
的第二个参数比check
更具协变性,因为它是&#39; sa参考
为什么没有'a
编译? check(&foo, &shortlived);
的有效期比foo
长。 &shortlived
的第二个参数是check
的协变,但它的第一个参数是{em>逆变而不是'a
,因为'a
是逆变的。也就是说,这两个参数都试图在此调用的相反方向拉Foo<'a>
:'a
正在尝试放大&foo
的生命周期(这是非法的),而{&shortlived
1}}试图缩短&shortlived
的生命周期(这也是非法的)。没有生命将统一这两个变量,因此调用无效。
1 这实际上可能是一种简化。我相信参考的生命周期参数实际上代表借用活动的区域,而不是参考的生命周期。在此示例中,对于包含对&foo
的调用的语句,两个借位都将处于活动状态,因此它们将具有相同的类型。但是,如果您将借用拆分为单独的ensure_equal
语句,则代码仍然有效,因此解释仍然有效。也就是说,为了使借用有效,所指对象必须比借用区域更长,所以当我想到终身参数时,我只关心指涉对象的生命,我认为借用是分开的的
答案 1 :(得分:3)
解释这一点的另一种方法是注意Foo
实际上并不包含对生命为'a
的任何内容的引用。相反,它包含 接受 具有生命期'a
的引用的功能。
您可以使用实际函数而不是PhantomData
来构造此相同的行为。你甚至可以调用这个函数:
struct Foo<'a>(fn(&'a ()));
fn hint<'a, Arg>(_: &'a Arg) -> Foo<'a> {
fn bar<'a, T: Debug>(value: &'a T) {
println!("The value is {:?}", value);
}
Foo(bar)
}
fn main() {
let outlived = ();
let foo;
{
let shortlived = ();
// &shortlived is borrowed by hint() but NOT stored in foo
foo = hint(&shortlived);
}
foo.0(&outlived);
}
弗朗西斯在他的优秀答案中解释说,outlived
的类型是shortlived
类型的子类型,因为它的生命周期更长。因此,foo
内的函数可以接受它,因为它可以被强制转换为shortlived
(更短)的生命周期。