在我偶然发现下面的代码之前,我确信类型的生命周期参数中的生命周期总是比其自己的实例更长.换句话说,给定一个foo: Foo<'a>,那么'a总是会活得更久foo.然后我被@Luc Danton(游乐场)介绍给这个反辩论代码:
#[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);
}
Run Code Online (Sandbox Code Playgroud)
尽管foo创建者hint似乎认为生命周期与其自身一样长,并且对它的引用被传递给更广泛范围内的函数,但代码完全按原样编译.取消注释代码中声明的行会触发编译错误.或者,更改Foo为struct tuple (PhantomData<&'a ()>)也会使代码不再使用相同类型的错误进行编译(Playground).
如何有效的Rust代码?这里编译器的原因是什么?
Fra*_*gné 46
尽管你的意图hint很好,但你的功能可能没有你期望的效果.但是,在我们了解正在发生的事情之前,我们还有很多理由可以解决.
让我们从这开始:
fn ensure_equal<'z>(a: &'z (), b: &'z ()) {}
fn main() {
let a = ();
let b = ();
ensure_equal(&a, &b);
}
Run Code Online (Sandbox Code Playgroud)
好的,所以main,我们定义了两个变量,a和b.由于被不同的let陈述引入,它们具有不同的生命期.ensure_equal需要两个具有相同生命周期的引用.然而,这段代码编译.为什么?
这是因为,由于'a: 'b(读:'a会超越'b),&'a T是一个亚型的&'b T.
假设ais 'a的寿命和寿命b是'b.这是事实'a: 'b,因为a首先介绍.在调用时ensure_equal,参数分别为1&'a ()和1.这里存在类型不匹配,因为并且生命周期不同.但是编译器还没有放弃!它知道这是一个子类型.换句话说,a 是a.因此,编译器将强制表达式键入,以便键入两个参数.这解决了类型不匹配的问题.&'b ()'a'b&'a ()&'b ()&'a () &'b ()&a&'b ()&'b ()
如果你对"子类型"的生命周期的应用感到困惑,那么让我用Java术语来重述这个例子.让我们来代替&'a ()用Programmer和&'b ()用Person.现在让我们说它Programmer来源于Person:Programmer因此是一个子类型Person.这意味着我们可以获取类型的变量Programmer并将其作为参数传递给期望参数类型的函数Person.这就是为什么下面的代码将成功编译:编译器将解析T为Person一个呼叫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);
}
}
Run Code Online (Sandbox Code Playgroud)
也许这种子类型关系的非直观方面是较长的寿命是较短寿命的子类型.但是可以这样想:在Java中,假装a Programmer是a是安全的Person,但是你不能认为a Person是a Programmer.同样,假设变量的生命周期较短是安全的,但您不能假设具有某些已知生命周期的变量实际上具有更长的生命周期.毕竟,Rust中的整个生命周期都是为了确保您不会访问超出实际生命周期的对象.
现在,我们来谈谈差异.那是什么?
方差是类型构造函数关于其参数的属性.Rust中的类型构造函数是具有未绑定参数的泛型类型.例如,
Vec是一个带有aT并返回a 的类型构造函数Vec<T>.&并且&mut是带有两个输入的类型构造函数:生命周期和指向的类型.
通常,您会期望a的所有元素Vec<T>具有相同的类型(我们在这里不讨论特征对象).但是差异让我们为此作弊.
&'a T是协变过'a和T.这意味着无论我们&'a T在类型参数中看到什么,我们都可以用它的子类型替换它&'a T.让我们看看它是如何工作的:
fn main() {
let a = ();
let b = ();
let v = vec![&a, &b];
}
Run Code Online (Sandbox Code Playgroud)
我们已经建立了它a并且b具有不同的生命周期,并且表达式&a和&b不具有相同的类型1.那么为什么我们可以用Vec这些呢?推理与上面的相同,所以我总结一下:&a是强制的&'b (),所以类型v是Vec<&'b ()>.
fn(T)在方差方面,Rust是一个特例.fn(T)是逆变的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();
}
Run Code Online (Sandbox Code Playgroud)
这编译.但是,什么是类型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(&());
}
Run Code Online (Sandbox Code Playgroud)
这不编译.以下是编译器消息:
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
Run Code Online (Sandbox Code Playgroud)
我们试图用参数调用向量中的一个&'a ()函数.但是v[0]需要一个&'static (),而且也不能保证'a是'static,所以这是无效的.因此,我们可以得出结论,类型v是Vec<fn(&'static ())>.正如您所看到的,逆变是与协方差相反的:我们可以用更长的寿命代替短寿命.
哇,现在回到你的问题.首先,让我们看一下编译器调用的内容hint.hint有以下签名:
fn hint<'a, Arg>(_: &'a Arg) -> Foo<'a>
Run Code Online (Sandbox Code Playgroud)
Foo在逆变过'a,因为Foo包装一个fn(或者更确切地说,假装,感谢PhantomData,但这并不有所作为,当我们谈论方差;两者具有相同效果),fn(T)是逆变过T,并且T在这里&'a ().
当编译器尝试解析调用时hint,它只考虑shortlived生命周期.因此,hint返回一个Foo具有shortlived的寿命.但是当我们尝试将它分配给变量时foo,我们遇到了一个问题:类型上的生命周期参数总是超过类型本身,并且它shortlived的生命周期不会超过foo生命周期,所以显然,我们不能使用该类型对foo.如果Foo是协变'a,那将是它的结束,你会得到一个错误.但是它Foo是逆变的'a,所以我们可以用更长的寿命来代替它shortlived的寿命.这一生可以是任何超过寿命的寿命.请注意,"outlives"与"严格超越"不同:不同之处在于(outlives )是真实的,但是严格的outlives 是错误的(即寿命据说比自己寿命长,但它并不严格超过自身).因此,我们可能最终有型,其中也正是寿命本身.foo'a: 'a'a'a'a'afooFoo<'a>'afoo
现在让我们来看看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语句,代码仍然有效,因此解释仍然有效.也就是说,为了使借用有效,指示对象必须比借用区域更长,所以当我考虑终身参数时,我只关心指示对象的生命周期,我认为借用是分开的.
解释这一点的另一种方法是注意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);
}
Run Code Online (Sandbox Code Playgroud)
正如弗朗西斯在他的精彩回答中所解释的那样, 的 类型outlived是 的 类型的子类型shortlived,因为它的生命周期更长。因此,内部函数foo可以接受它,因为它可以被强制为 的shortlived(较短的)生命周期。
| 归档时间: |
|
| 查看次数: |
1906 次 |
| 最近记录: |