4.5.3.1 基本准则
译文 · 基于 Learning Rust
基本准则
原文链接: https://quinedot.github.io/rust-learning/dyn-elision-basic.html
提醒:dyn Trait 是由生命周期参数化的类型构造器;完全解析的类型包含生命周期,如 dyn Trait + 'static。生命周期在许多情况下可省略,此时实际使用的生命周期可能是某个默认生命周期,或经推断得出。
讨论默认 trait 对象(dyn Trait)生命周期时,我们指生命周期被完全省略的情况。若使用通配符生命周期(dyn Trait + '_),则通常适用普通生命周期省略规则。(例外很少,需要时通常可以显式写出。)
对于完全省略的 dyn Trait 生命周期,对没有生命周期约束的 trait(占绝大多数),可从以下一般准则开始:
- 在函数体内,trait 对象生命周期由推断决定(即忽略以下条目)
- 对于
&'a dyn Trait 等引用,默认与引用生命周期相同('a) - 对于带生命周期参数的
dyn 支持 std 类型,如 Ref<'a, T>,也是 'a - 对于无生命周期参数的类型如
Box<dyn Trait>,以及裸 dyn Trait,为 'static
对于(罕见的)带生命周期约束的 trait:
- 若 trait 有
'static 约束,trait 对象生命周期总是 'static - 若 trait 只有非
'static 生命周期约束,最好显式写出
这足以在大多数情况下理解 dyn Trait 生命周期省略,但这些准则有例外(在接下来几页探讨)。
这些准则内部也有一些细微差别,下面立即说明。
默认 'static 的陷阱
最可能遇到 dyn Trait 生命周期错误的情况是涉及 Box 等,导致隐式 'static 约束。
这些错误通常可通过添加显式 'static 约束,或覆盖隐式 'static 生命周期来解决。特别是使用 '_ 通常会在给定上下文中得到「普通」(非 dyn Trait)生命周期省略。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| trait Trait {}
impl<T: Trait> Trait for &T {}
// 去掉 `+ 'static` 可看错误
fn with_explicit_bound<'a, T: Trait + 'static> (t: T) -> Box<dyn Trait> {
Box::new(t)
}
// 去掉 `+ 'a`(任一位置)可看错误
fn with_nonstatic_box<'a, T: Trait + 'a>(t: T) -> Box<dyn Trait + 'a> {
Box::new(t)
}
// 去掉 `+ '_` 可看错误
fn with_fn_lifetime_elision(t: &impl Trait) -> Box<dyn Trait + '_> {
Box::new(t)
}
|
在函数体内这可能特别令人困惑:Box<dyn Trait> 变量注解与 Box<dyn Trait> 函数输入参数注解行为不同:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # trait Trait {}
# impl Trait for &i32 {}
// 在此上下文中,省略的生命周期是 `'static`
fn requires_static(_: Box<dyn Trait>) {}
fn example() {
let local = 0;
// 在此上下文中,注解表示 `Box<dyn Trait + '_>`!
// 因此单独用局部引用可以编译。
let bx: Box<dyn Trait> = Box::new(&local);
// 因此尽管语法相同,此调用不能编译。
// 取消注释可看编译错误。
// requires_static(bx);
}
|
dyn Trait 生命周期省略适用于 impl 头,可能导致实现不如可能或期望的那样通用:
1
2
3
4
5
| trait Trait {}
trait Two {}
impl Two for Box<dyn Trait> {}
impl Two for &dyn Trait {}
|
Two 为以下类型实现:
Box<dyn Trait + 'static>- 任意
'a 的 &'a (dyn Trait + 'a)(生命周期必须匹配)
若可能,考虑如下实现,它们更通用:
1
2
3
4
5
6
7
| # trait Trait {}
# trait Two {}
// 对所有生命周期实现
impl Two for Box<dyn Trait + '_> {}
// 对所有内层生命周期至少与外层一样长的生命周期实现
impl Two for &(dyn Trait + '_) {}
|
类型别名陷阱
与 impl 头类似,定义类型别名时也会应用省略:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| trait MyTraitICouldNotThinkOfAShortNameFor {}
// 这是 `dyn ... + 'static` 的别名!
type MyDyn = dyn MyTraitICouldNotThinkOfAShortNameFor;
// 默认不会「覆盖」类型别名,因此
// 要求 trait 对象生命周期为 `'static`
fn foo(_: &MyDyn) {}
// 根据 `dyn` 省略规则,这要求 trait 对象生命周期
// 与引用相同...
fn bar(d: &dyn MyTraitICouldNotThinkOfAShortNameFor) {
// ...因此失败,因为生命周期无法延长
foo(d);
}
|
更一般地,省略不会「穿透」或改变类型别名。包括实现块内的 Self 别名。
1
2
3
4
5
6
7
8
9
10
11
| trait Trait {}
impl dyn Trait {
// 错误:要求 `T: 'static`
fn f<T: Trait>(t: &T) -> &Self { t }
}
impl<'a> dyn Trait + 'a {
// 错误:要求 `T: 'a`
fn g<T: Trait>(t: &T) -> &Self { t }
}
|
另见带参数的类型别名如何行为。
'static trait
当 trait 本身是 'static 时,trait 对象生命周期有隐式 'static 约束。因此若显式命名 trait 对象生命周期,给出的名称也会有隐式 'static 约束。因此:
1
2
3
4
5
6
7
8
| # use core::any::Any;
// 注:trait `Any` 有 `'static` 约束
fn example<'a>(_: &'a (dyn Any + 'a)) {}
fn main() {
let local = ();
example(&local);
}
|
得到错误:local 的借用必须是 'static。问题是 example 中的 'a 继承了 'static 约束('a: 'static),且外层引用也给了生命周期 'a。这是我们实际上不希望它们相同的情况。
最符合人体工程学的解决方案是:当 trait 本身有 'static 约束时,总是完全省略 trait 对象生命周期。与其他情况不同,当 trait 本身有 'static 约束时,trait 对象生命周期与外层引用生命周期独立,因此以下可编译:
1
2
3
4
5
6
7
8
| # use core::any::Any;
// 这是 `&'a (dyn Any + 'static)`,且 `'a` 不必是 `'static`
fn example(_: &dyn Any) {}
fn main() {
let local = ();
example(&local);
}
|
Any 是最常见的带 'static 约束的 trait,即最可能遇到此场景的原因。
static 上下文
在声明 static 等某些上下文中,可以省略引用等类型的生命周期;这样做通常会使省略的生命周期为 'static:
1
2
3
| // 省略的生命周期是 `'static`
static S: &str = "";
const C: &str = "";
|
在远古时代, 必须写出 'static 生命周期。自 Rust 1.17 起,在模块顶层省略生命周期是 'static 的糖。
因此省略的 dyn Trait 生命周期默认也是 'static:
1
2
3
4
5
| # trait Trait {}
# impl Trait for () {}
// 这些是 `&'static (dyn Trait + Sync + 'static)`。
static S: &(dyn Trait + Sync) = &();
const C: &(dyn Trait + Sync) = &();
|
遗憾的是,其他上下文的行为不够一致。例如:
1
2
3
4
5
6
| # trait Trait {}
# impl Trait for () {}
struct S<'a: 'b, 'b>(&'b &'a str);
impl<'a: 'b, 'b> S<'a, 'b> {
const T: &(dyn Trait + 'a) = &();
}
|
Rust 1.64 之前需要显式生命周期,之后生成新的匿名生命周期。自 Rust 1.81 起触发默认拒绝的未来兼容性 lint。
至少有几次无意中使 const 上下文中省略的生命周期为 'static。例如以下片段在 Rust 1.64 到 1.70 无法编译,因为生命周期被脱糖为 'static。
1
2
3
4
5
6
7
8
| fn foo() -> [(); {
// (这是 `const` 上下文)
let a = 10_usize;
let b: &usize = &a;
*b
}] {
[(); 10]
}
|
在这些上下文中,再次通常省略所有生命周期就是你想要的。因此若在此上下文遇到生命周期错误,最好建议是先省略一切,若失败再显式写出一切。