4.5.1 型变
译文 · 基于 Learning Rust
型变
原文链接: https://quinedot.github.io/rust-learning/dyn-covariance.html
dyn Trait 的生命周期是协变的,类似引用的外层生命周期。这意味着每当它处于协变类型位置时,较长的生命周期可以强制转换为较短的生命周期。
1
2
3
4
| # trait Trait {}
fn why_be_static<'a>(bx: Box<dyn Trait + 'static>) -> Box<dyn Trait + 'a> {
bx
}
|
较长生命周期的 trait 对象是较短生命周期 trait 对象的子类型,因此这是一种超类型强制转换。下一节将探讨 trait 对象子类型的另一种形式。
trait 对象生命周期协变的原因在于:该生命周期表示仍可对 trait 对象调用方法的区域。既然在该区域的任何地方调用方法都合法,将区域限制为自身的某个子集也合法——即将生命周期强制转换为更短。
然而,dyn Trait 的生命周期实际上比典型的协变生命周期更灵活。
不变上下文中的 unsizing 强制转换
我们之前提到,可以将 dyn Trait + 'a 强制转换为 dyn Trait + 'b,其中 'a: 'b。这不就是协变吗?不完全是——之前说的是两个 dyn Trait + '_ 之间的 unsizing 强制转换。
该强制转换甚至可以在不变位置发生。 这意味着 dyn Trait 的生命周期在不变上下文中也能以类似协变的方式表现!
例如,以下能编译,尽管 dyn Trait 在 &mut 之后:
1
2
3
4
5
6
7
8
| # trait Trait {}
fn invariant_coercion<'m, 'long: 'short, 'short>(
arg: &'m mut (dyn Trait + 'long)
) ->
&'m mut (dyn Trait + 'short)
{
arg
}
|
但由于没有嵌套的 unsizing 强制转换,以下版本不能编译:
1
2
3
4
5
6
| # use std::cell::Cell;
# trait Trait {}
// 失败:`Cell<T>` 对 `T` 不变,且 `dyn Trait` 嵌套
fn foo<'l: 's, 's>(v: Cell<Box<Box<dyn Trait + 'l>>>) -> Cell<Box<Box<dyn Trait + 's>>> {
v
}
|
因为这是 unsizing 强制转换而非子类型强制转换,某些情况下必须显式强制转换,例如使用 as。
1
2
3
4
5
6
| # trait Trait {}
// 失败:闭包返回 `Option<&'a mut (dyn Trait + 'static)>`,
// unsizing 强制转换无法在 `Option<&mut _>` 内嵌套应用。
fn foo<'a>(arg: &'a mut Box<dyn Trait + 'static>) -> Option<&'a mut (dyn Trait + 'a)> {
true.then(move || arg.as_mut())
}
|
1
2
3
4
5
| # trait Trait {}
// 添加显式 `as` 后示例可编译。
fn foo<'a>(arg: &'a mut Box<dyn Trait + 'static>) -> Option<&'a mut (dyn Trait + 'a)> {
true.then(move || arg.as_mut() as _)
}
|
(尽管可以说这个特定例子是个 bug)。
为何这实际上是关键特性
我们很快会深入探讨省略的生命周期,但在此注意,这种「超强协变」对使常见模式足够好用非常重要。
以下例子中 foo 和 bar 的签名实际上相同:
1
2
3
4
5
6
7
| # trait Trait {}
fn foo(d: &mut dyn Trait) {}
fn bar<'a>(d: &'a mut (dyn Trait + 'a)) {
foo(d);
foo(d);
}
|
我们可以通过重借用 &'a mut dyn Trait 得到短于 'a 的借用,从而在 bar 中多次调用 foo。但此情况下 trait 对象生命周期必须与外层 &mut 生命周期匹配,因此还必须将 dyn Trait + 'a 强制转换为该更短的生命周期。
在 &mut Box<dyn Trait> 与 &mut dyn Trait 之间切换时也有类似考虑:
1
2
3
4
5
6
7
8
9
10
11
12
| # trait Trait {}
# fn foo(d: &mut dyn Trait) {}
# fn bar<'a>(d: &'a mut (dyn Trait + 'a)) {
# foo(d);
# foo(d);
# }
fn baz(bx: &mut Box<dyn Trait /* + 'static */>) {
// 若 trait 对象生命周期不能在 `&mut` 内「收缩」,
// 这些调用根本无法进行
foo(&mut **bx);
bar(&mut **bx);
}
|
这里将 **bx 重借用为 &'a mut (dyn Trait + 'static)(某个短命的 'a),再强制转换为 &'a mut (dyn Trait + 'a)。
嵌套上下文中的型变
当 'a: 'b 时,从 dyn Trait + 'a 到 dyn Trait + 'b 的超类型强制转换可以在深层嵌套上下文中发生,只要 trait 对象仍处于协变上下文。因此与上面的 Cell 版本不同,此例可编译:
1
2
3
4
| # trait Trait {}
fn foo<'l: 's, 's>(v: Vec<Box<Box<dyn Trait + 'l>>>) -> Vec<Box<Box<dyn Trait + 's>>> {
v
}
|