尽管你的 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 ()
1。这里存在类型不匹配的问题,因为'a
和'b
的生命周期不同。但是编译器并没有放弃!它知道&'a ()
是&'b ()
的一个子类型。换句话说,&'a ()
是 &'b ()
。编译器将强制将表达式&a
转换为类型&'b ()
,以使两个参数都具有类型&'b ()
。这解决了类型不匹配的问题。
如果您对具有生命周期的"子类型"的应用感到困惑,那么让我用Java术语重新表述这个例子。我们将&'a ()
替换为Programmer
,将&'b ()
替换为Person
。现在假设Programmer
是从Person
派生出来的:因此,Programmer
是Person
的一个子类型。这意味着我们可以取一个类型为Programmer
的变量,并将其作为期望参数类型为Person
的函数的参数传递。这就是为什么以下代码将成功编译的原因:编译器将在main
中将T
解析为Person
。
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中,假设一个程序员
是一个人
是安全的,但你不能假定一个人
就是一个程序员
。同样的,在Rust中假设一个变量具有短的生命周期是安全的,但你不能假定具有某个已知生命周期的变量实际上具有长的生命周期。毕竟,Rust中生命周期的整个目的是确保你不会访问超过它们实际生命周期的对象。
现在,让我们来谈谈协变性。那是什么?
协变性是类型构造函数相对于其参数所具有的属性。在Rust中,类型构造函数是具有未绑定参数的通用类型。例如,Vec
是一个接受一个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的类型不同
1。那么为什么我们可以用它们来创建一个Vec呢?推理与上面相同,所以我会总结一下:&a被强制转换为&'b(),因此v的类型是Vec<&'b()>。
在Rust中,fn(T)是一个特殊情况,涉及到方差。对于T,fn(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();
}
这个代码可以编译通过。但是在quux
函数中,v
的类型是什么呢?它是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
,但是在讨论协变性时这并没有什么区别;两者具有相同的效果),fn(T)
针对 T
逆变,并且这里的 T
是 &'a ()
。
当编译器尝试解析对 hint
的调用时,它只考虑了 shortlived
的生命周期。因此,hint
返回一个带有 shortlived
生命周期的 Foo
。但是当我们尝试将其分配给变量 foo
时,就会出现问题:类型上的生命周期参数总是比类型本身更长寿,而 shortlived
的生命周期不会比 foo
的生命周期更长寿,因此,显然,我们不能使用那种类型来代替 foo
。如果 Foo
针对 'a
协变,那就没戏了,你会得到一个错误。但是 Foo
是针对 'a
逆变的,因此我们可以将 shortlived
的生命周期替换为一个更大的生命周期。该生命周期可以是任何比 foo
的生命周期更长寿的生命周期。请注意,“更长寿”并不等同于“严格更长寿”:区别在于 'a: 'a
('a
比 'a
更长寿)是正确的,但是 'a
严格比 'a
更长寿是错误的(即生命周期被认为比自己更长寿,但它并没有“严格地比自己更长寿”)。因此,我们可能会得到类型为 Foo<'a>
的 foo
,其中 'a
正好是 foo
本身的生命周期。
现在让我们看一下 check(&foo, &outlived);
(这是第二个)。这个函数编译通过,因为 &outlived
被强制转换,以使其生命周期缩短以匹配 foo
的生命周期。这是有效的,因为 outlived
的生命周期比 foo
更长寿,并且 check
的第二个参数针对 'a
协变,因为它是一个引用。
为什么
check(&foo, &shortlived);
不能编译?因为
foo
的生命周期比
&shortlived
长。
check
的第二个参数对于
'a
具有协变性,但是它的第一个参数对于
'a
来说是
逆变的,因为
Foo<'a>
是逆变的。也就是说,对于这个调用,这两个参数都试图将
'a
拉向相反的方向:
&foo
试图扩大
&shortlived
的生命周期(这是非法的),而
& shortlived
则试图缩短
&foo
的生命周期(这也是非法的)。没有生命周期可以统一这两个变量,因此这个调用是无效的。
1 实际上,这可能是一个简化。我认为参考的生命周期参数实际上代表借用处于活动状态的区域,而不是引用的生命周期。在这个例子中,两个借用在包含对ensure_equal
的调用的语句中都是活动状态,因此它们具有相同的类型。但是如果你将借用拆分到不同的let
语句中,代码仍然可以工作,因此解释仍然有效。也就是说,为了使借用有效,引用必须超出借用的区域的生命周期,因此当我考虑生命周期参数时,我只关心参考对象的生命周期,并将借用单独处理。
fn(T)
在T
中是逆变的--然而,我还没有完全解释为什么。 - trent