问题描述
在我偶然发现下面的代码之前,我确信类型的生命周期参数中的生命周期总是比它自己的实例生命周期长.换句话说,给定一个 foo: Foo
,那么 'a
将永远比 foo
存活时间长.然后@Luc Danton (游乐场):
Before I stumbled upon the code below, I was convinced that a lifetime in a type's lifetime parameter would always outlive its own instances. In other words, given a foo: Foo<'a>
, then 'a
would always outlive foo
. Then I was introduced to this counter-argument code by @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);
}
即使由 hint
创建的 foo
似乎考虑了一个生命周期不长于它自己,并且对它的引用传递给了一个函数范围更广,代码完全按原样编译.取消注释代码中声明的行会触发编译错误.或者,将 Foo
更改为结构元组 (PhantomData)
也会使代码不再编译时出现相同类型的错误 (
Even though the foo
created by hint
appears to consider a lifetime that does not live for as long as itself, and a reference to it is passed to a function in a wider scope, the code compiles exactly as it is. Uncommenting the line stated in the code triggers a compilation error. Alternatively, changing Foo
to the struct tuple (PhantomData<&'a ()>)
also makes the code no longer compile with the same kind of error (Playground).
它如何是有效的 Rust 代码?这里编译器的推理是什么?
How is it valid Rust code? What is the reasoning of the compiler here?
推荐答案
尽管您的意图是最好的,但您的 hint
函数可能不会达到您预期的效果.但是,在我们了解正在发生的事情之前,我们还有很多地方需要研究.
Despite your best intentions, your hint
function may not have the effect you expect. But we have quite a bit of ground to cover before we can understand what's going on.
让我们从这个开始:
fn ensure_equal<'z>(a: &'z (), b: &'z ()) {}
fn main() {
let a = ();
let b = ();
ensure_equal(&a, &b);
}
好的,所以在main
中,我们定义了两个变量,a
和b
.由于由不同的 let
语句引入,它们具有不同的生命周期.ensure_equal
需要两个具有相同生命周期的引用.然而,这段代码可以编译.为什么?
OK, so in main
, we defining two variables, a
and b
. They have distinct lifetimes, by virtue of being introduced by distinct let
statements. ensure_equal
requires two references with the same lifetime. And yet, this code compiles. Why?
那是因为,给定 'a: 'b
(读作:'a
比 'b
寿命更长),&'aT
是一个 子类型&'b T
.
That's because, given 'a: 'b
(read: 'a
outlives 'b
), &'a T
is a subtype of &'b T
.
假设 a
的生命周期是 'a
,而 b
的生命周期是 'b
.'a: 'b
是事实,因为 a
是最先引入的.在调用 ensure_equal
时,参数类型分别为 &'a ()
和 &'b ()
,分别.这里存在类型不匹配,因为 'a
和 'b
的生命周期不同.但是编译器还没有放弃!它知道 &'a ()
是 &'b ()
的子类型.换句话说,一个&'a()
就是一个 &'b()
.因此,编译器将强制表达式 &a
键入 &'b ()
,以便两个参数都键入 &'b ()代码>.这解决了类型不匹配.
Let's say the lifetime of a
is 'a
and the lifetime of b
is 'b
. It's a fact that 'a: 'b
, because a
is introduced first. On the call to ensure_equal
, the arguments are typed &'a ()
and &'b ()
, respectively. There's a type mismatch here, because 'a
and 'b
are not the same lifetime. But the compiler doesn't give up yet! It knows that &'a ()
is a subtype of &'b ()
. In other words, a &'a ()
is a &'b ()
. The compiler will therefore coerce the expression &a
to type &'b ()
, so that both arguments are typed &'b ()
. This resolves the type mismatch.
如果您对子类型"与生命周期的应用感到困惑,那么让我用 Java 术语重新表述这个示例.让我们将 &'a ()
替换为 Programmer
并将 &'b ()
替换为 Person
.现在假设 Programmer
派生自 Person
:因此 Programmer
是 Person
的子类型.这意味着我们可以将 Programmer
类型的变量作为参数传递给需要 Person
类型参数的函数.这就是为什么下面的代码会成功编译的原因:编译器会将 T
解析为 Person
以在 main
中调用.
If you're confused by the application of "subtypes" with lifetimes, then let me rephrase this example in Java terms. Let's replace &'a ()
with Programmer
and &'b ()
with Person
. Now let's say that Programmer
is derived from Person
: Programmer
is therefore a subtype of Person
. That means that we can take a variable of type Programmer
and pass it as an argument to a function that expects a parameter of type Person
. That's why the following code will successfully compile: the compiler will resolve T
as Person
for the call in 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
> 是程序员
.同样,假设一个变量的生命周期较短是安全的,但是你不能假设一个已知生命周期的变量实际上有一个更长的生命周期.毕竟,Rust 生命周期的全部意义在于确保您不会访问超出其实际生命周期的对象.
Perhaps the non-intuitive aspect of this subtyping relation is that the longer lifetime is a subtype of the shorter lifetime. But think of it this way: in Java, it's safe to pretend that a Programmer
is a Person
, but you can't assume that a Person
is a Programmer
. Likewise, it's safe to pretend that a variable has a shorter lifetime, but you can't assume that a variable with some known lifetime actually has a longer lifetime. After all, the whole point of lifetimes in Rust is to ensure that you don't access objects beyond their actual lifetime.
现在,让我们谈谈方差.那是什么?
Now, let's talk about variance. What's that?
Variance 是类型构造函数关于其参数的一个属性.Rust 中的类型构造函数是具有未绑定参数的泛型类型.例如,Vec
是一个类型构造函数,它接受一个 T
并返回一个 Vec
.&
和 &mut
是接受两个输入的类型构造函数:生命周期和要指向的类型.
通常,您希望 Vec<T>
的所有元素都具有相同的类型(这里我们不是在讨论 trait 对象).但是差异让我们在这一点上作弊.
Normally, you would expect all elements of a Vec<T>
to have the same type (and we're not talking about trait objects here). But variance lets us cheat with that.
&'a T
在 'a
和 T
上协变.这意味着无论我们在类型参数中看到 &'a T
的任何地方,我们都可以用 &'a T
的子类型替换它.让我们看看它是如何工作的:
&'a T
is covariant over 'a
and T
. That means that wherever we see &'a T
in a type argument, we can substitute it with a subtype of &'a T
. Let's see how it works out:
fn main() {
let a = ();
let b = ();
let v = vec![&a, &b];
}
我们已经确定 a
和 b
具有不同的生命周期,并且表达式 &a
和 &b
没有相同的类型.那么为什么我们可以用这些来制作 Vec
呢?道理和上面一样,所以总结一下:&a
被强制转换为&'b()
,这样v的类型
是 Vec
.
We've already established that a
and b
have different lifetimes, and that the expressions &a
and &b
don't have the same type. So why can we make a Vec
out of these? The reasoning is the same as above, so I'll summarize: &a
is coerced to &'b ()
, so that the type of v
is Vec<&'b ()>
.
fn(T)
是 Rust 中的一个特殊情况,当涉及到方差时.fn(T)
与 T
是 逆变.让我们构建一个Vec
函数!
fn(T)
is a special case in Rust when it comes to variance. fn(T)
is contravariant over T
. Let's build a Vec
of functions!
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
还是Vec
?
This compiles. But what's the type of v
in quux
? Is it Vec<fn(&'static ())>
or 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(&());
}
这不能编译.以下是编译器消息:
This doesn't compile. Here are the compiler messages:
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
.如您所见,逆变与协方差相反:我们可以用更长的生命周期替换较短的生命周期.
We're trying to call one of the functions in the vector with a &'a ()
argument. But v[0]
expects a &'static ()
, and there's no guarantee that 'a
is 'static
, so this is invalid. We can therefore conclude that the type of v
is Vec<fn(&'static ())>
. As you can see, contravariance is the opposite of covariance: we can replace a short lifetime with a longer one.
哇,现在回到你的问题.首先,让我们看看编译器对 hint
的调用做了什么.hint
具有以下签名:
Whew, now back to your question. First, let's see what the compiler makes out of the call to hint
. hint
has the following signature:
fn hint<'a, Arg>(_: &'a Arg) -> Foo<'a>
Foo
与 'a
是 逆变 因为 Foo
包装了一个 fn
(或者更确切地说,假装,感谢 PhantomData
,但是当我们谈论方差时,这并没有什么不同;两者都有相同的效果),fn(T)
与 T
是逆变的,而 T
在这里是 &'a ()
.
Foo
is contravariant over 'a
because Foo
wraps a fn
(or rather, pretends to, thanks to the PhantomData
, but that doesn't make a difference when we talk about variance; both have the same effect), fn(T)
is contravariant over T
and that T
here is &'a ()
.
当编译器尝试解析对hint
的调用时,它只考虑shortlived
的生命周期.因此,hint
返回一个 Foo
和 shortlived
的生命周期.但是当我们尝试将它分配给变量 foo
时,我们遇到了一个问题:类型上的生命周期参数总是比类型本身更长寿,而 shortlived
的生命周期不会't 超过 foo
的生命周期,所以很明显,我们不能对 foo
使用该类型.如果 Foo
在 'a
上是协变的,那么它就结束了,你会得到一个错误.但是 Foo
与 'a
相比 逆变,所以我们可以用 更大的生命周期替换 shortlived
的生命周期 生命周期.该生命周期可以是任何比 foo
生命周期更长的生命周期.请注意,outlives"与strictly outlives"不同:区别在于 'a: 'a
('a
比 'a
>) 是真的,但是 'a
严格来说比 'a
的寿命长是假的(也就是说,一个生命周期被认为比它本身的寿命长,但它并不严格地长于em> 本身).因此,我们可能会得到 foo
类型为 Foo
的结果,其中 'a
正是 foo的生命周期代码>本身.
When the compiler tries to resolve the call to hint
, it only considers shortlived
's lifetime. Therefore, hint
returns a Foo
with shortlived
's lifetime. But when we try to assign that to the variable foo
, we have a problem: a lifetime parameter on a type always outlives the type itself, and shortlived
's lifetime doesn't outlive foo
's lifetime, so clearly, we can't use that type for foo
. If Foo
was covariant over 'a
, that would be the end of it and you'd get an error. But Foo
is contravariant over 'a
, so we can replace shortlived
's lifetime with a larger lifetime. That lifetime can be any lifetime that outlives foo
's lifetime. Note that "outlives" is not the same as "strictly outlives": the difference is that 'a: 'a
('a
outlives 'a
) is true, but 'a
strictly outlives 'a
is false (i.e. a lifetime is said to outlive itself, but it doesn't strictly outlive itself). Therefore, we might end up with foo
having type Foo<'a>
where 'a
is exactly the lifetime of foo
itself.
现在让我们看看 check(&foo, &outlived);
(这是第二个).这个编译是因为 &outlived
被强制,所以生命周期被缩短以匹配 foo
的生命周期.这是有效的,因为 outlived
比 foo
有更长的生命周期,并且 check
的第二个参数在 'a
上是协变的因为它是一个参考.
Now let's look at check(&foo, &outlived);
(that's the second one). This one compiles because &outlived
is coerced so that the lifetime is shortened to match foo
's lifetime. That's valid because outlived
has a longer lifetime than foo
, and check
's second argument is covariant over 'a
because it's a reference.
为什么 check(&foo, &shortlived);
不能编译?foo
的生命周期比 &shortlived
更长.check
的第二个参数在 'a
上是协变的,但它的第一个参数是 'a
上的 逆变,因为Foo
是逆变的.也就是说,这两个参数都试图将 'a
拉到相反的方向来进行这个调用:&foo
正在尝试扩大 &shortlived
's 生命周期(这是非法的),而 &shortlived
试图缩短 &foo
的生命周期(这也是非法的).没有生命周期将统一这两个变量,因此调用无效.
Why doesn't check(&foo, &shortlived);
compile? foo
has a longer lifetime than &shortlived
. check
's second argument is covariant over 'a
, but its first argument is contravariant over 'a
, because Foo<'a>
is contravariant. That is, both arguments are trying to pull 'a
in opposite directions for this call: &foo
is trying to enlarge &shortlived
's lifetime (which is illegal), while &shortlived
is trying to shorten &foo
's lifetime (which is also illegal). There is no lifetime that will unify these two variables, therefore the call is invalid.
这实际上可能是一种简化.我相信引用的生命周期参数实际上代表了借用处于活动状态的区域,而不是引用的生命周期.在这个例子中,两个借用对于包含对 ensure_equal
的调用的语句都是活动的,因此它们将具有相同的类型.但是,如果将借用拆分为单独的 let
语句,代码仍然有效,因此解释仍然有效.也就是说,要使借用有效,所指对象必须比借用区域的寿命长,因此当我考虑生命周期参数时,我只关心所指对象的生命周期,并且单独考虑借用.
That might actually be a simplification. I believe that the lifetime parameter of a reference actually represents the region in which the borrow is active, rather than the lifetime of the reference. In this example, both borrows would be active for the statement that contains the call to ensure_equal
, so they would have the same type. But if you split the borrows to separate let
statements, the code still works, so the explanation is still valid. That said, for a borrow to be valid, the referent must outlive the borrow's region, so when I'm thinking of lifetime parameters, I only care about the referent's lifetime and I consider borrows separately.
这篇关于这个实例怎么可能比它自己的参数生命周期长?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!