请考虑以下代码:
trait Trait<T> {}
fn foo<'a>(_b: Box<Trait<&'a usize>>) {}
fn bar(_b: Box<for<'a> Trait<&'a usize>>) {}
foo
和bar
这两个函数似乎都接受Box<Trait<&'a usize>>
,但foo
比bar
更简洁。他们之间有什么区别?
此外,在什么情况下我需要for<>
语法如上所述?我知道Rust标准库在内部使用它(通常与闭包有关),但为什么我的代码需要它呢?
答案 0 :(得分:65)
for<>
语法称为排名较高的特质绑定(HRTB),并且确实是因为闭包而引入的。
简而言之,foo
和bar
之间的区别在于foo()
内部usize
引用的生命周期由来电者提供 函数的>,在{{1}}中,函数本身提供相同的生命周期。这种区别对于bar()
/ foo
的实施非常重要。
但是,在这种特殊情况下,当bar
没有使用类型参数的方法时,这种区别是没有意义的,所以让我们假设Trait
看起来像这样:
Trait
请记住,生命周期参数与泛型类型参数非常相似。使用泛型函数时,始终指定其所有类型参数,提供具体类型,并且编译器将函数单态化。生命周期参数也是如此:当你调用一个具有生命周期参数的函数时,你指定生命周期,尽管是隐含的:
trait Trait<T> {
fn do_something(&self, value: T);
}
现在// imaginary explicit syntax
// also assume that there is TraitImpl::new::<T>() -> TraitImpl<T>,
// and TraitImpl<T>: Trait<T>
'a: {
foo::<'a>(Box::new(TraitImpl::new::<&'a usize>()));
}
可以对此值进行限制,即可以使用哪个参数调用foo()
。例如,这不会编译:
do_something()
这不会编译,因为局部变量的生命周期严格小于生命周期参数指定的生命周期(我认为很清楚为什么会这样),因此你不能调用fn foo<'a>(b: Box<Trait<&'a usize>>) {
let x: usize = 10;
b.do_something(&x);
}
因为它需要它的参数有生命b.do_something(&x)
,严格地大于'a
。
但是,您可以使用x
:
bar
这是有效的,因为现在fn bar(b: Box<for<'a> Trait<&'a usize>>) {
let x: usize = 10;
b.do_something(&x);
}
可以选择所需的生命周期而不是bar
的来电者。
当您使用接受引用的闭包时,这很重要。例如,假设您要在bar
上编写filter()
方法:
Option<T>
此处的闭包必须接受对impl<T> Option<T> {
fn filter<F>(self, f: F) -> Option<T> where F: FnOnce(&T) -> bool {
match self {
Some(value) => if f(&value) { Some(value) } else { None }
None => None
}
}
}
的引用,否则将无法返回选项中包含的值(这与迭代器上的T
的推理相同)。
但是filter()
中&T
的生命周期应该是多少?请记住,我们没有在功能签名中指定生命周期,因为有适当的生命周期;实际上,编译器为函数签名中的每个引用插入一个生命周期参数。 因此,扩展上述签名的最“明显”方式是:
FnOnce(&T) -> bool
但是,这不会起作用。与上面&T
的示例一样,生命周期FnOnce(&T) -> bool
严格地长,而不是此函数中任何局部变量的生命周期,包括匹配语句中的fn filter<'a, F>(self, f: F) -> Option<T> where F: FnOnce(&'a T) -> bool
。因此,由于终身不匹配,无法将Trait
应用于'a
。用这种签名编写的上述函数将无法编译。
另一方面,如果我们像这样扩展value
的签名(这实际上是关于封锁的生命周期省略如何在Rust中工作):
f
然后以&value
作为参数调用filter()
完全有效:我们现在可以选择生命周期,因此使用局部变量的生命周期绝对没问题。这就是为什么HRTB很重要的原因:没有它们,你将无法表达很多有用的模式。
您还可以在Nomicon中阅读有关HRTB的其他说明。