4.7.6 擦除 Trait
译文 · 基于 Learning Rust
擦除 Trait
原文链接: https://quinedot.github.io/rust-learning/dyn-trait-erased.html
假设你有一个现有 trait,大部分情况下工作良好:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| pub mod useful {
pub trait Iterable {
type Item;
type Iter<'a>: Iterator<Item = &'a Self::Item> where Self: 'a;
fn iter(&self) -> Self::Iter<'_>;
fn visit<F: FnMut(&Self::Item)>(&self, mut f: F) {
for item in self.iter() {
f(item);
}
}
}
impl<I: Iterable + ?Sized> Iterable for &I {
type Item = <I as Iterable>::Item;
type Iter<'a> = <I as Iterable>::Iter<'a> where Self: 'a;
fn iter(&self) -> Self::Iter<'_> {
<I as Iterable>::iter(*self)
}
}
impl<I: Iterable + ?Sized> Iterable for Box<I> {
type Item = <I as Iterable>::Item;
type Iter<'a> = <I as Iterable>::Iter<'a> where Self: 'a;
fn iter(&self) -> Self::Iter<'_> {
<I as Iterable>::iter(&**self)
}
}
impl<T> Iterable for Vec<T> {
type Item = T;
type Iter<'a> = std::slice::Iter<'a, T> where Self: 'a;
fn iter(&self) -> Self::Iter<'_> {
<[T]>::iter(self)
}
}
}
|
然而,它并非 dyn 兼容,而你希望它是。即使将来某天 dyn Trait 支持 GAT,也没有计划支持像 Iterable::visit 这样带泛型类型参数的方法。何况你想要的是现在就能用,而非「某一天」。
也许你还有大量使用该实用 trait 的代码,不想全部重做。也许它甚至不是你的 trait。
这种情况下,你可能想提供一个「擦除」版本的 trait 以使其 dyn 兼容。基本思路是用 dyn(类型擦除)替换所有不兼容 dyn 的用法,例如 GAT 和带类型参数的方法。
1
2
3
4
5
6
7
8
9
10
| pub mod erased {
// 此 trait 是 `dyn` 兼容的
pub trait Iterable {
type Item;
// 不再有 GAT
fn iter(&self) -> Box<dyn Iterator<Item = &Self::Item> + '_>;
// 不再有类型参数
fn visit(&self, f: &mut dyn FnMut(&Self::Item));
}
}
|
我们希望能从任何 useful::Iterable 创建 dyn erased::Iterable,因此需要 blanket 实现来连接两者:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| # fn main() {}
# pub mod useful {
# pub trait Iterable {
# type Item;
# type Iter<'a>: Iterator<Item = &'a Self::Item> where Self: 'a;
# fn iter(&self) -> Self::Iter<'_>;
# fn visit<F: FnMut(&Self::Item)>(&self, f: F);
# }
# }
pub mod erased {
use crate::useful;
# pub trait Iterable {
# type Item;
# fn iter(&self) -> Box<dyn Iterator<Item = &Self::Item> + '_>;
# fn visit(&self, f: &mut dyn FnMut(&Self::Item)) {
# for item in self.iter() {
# f(item);
# }
# }
# }
impl<I: useful::Iterable + ?Sized> Iterable for I {
type Item = <I as useful::Iterable>::Item;
fn iter(&self) -> Box<dyn Iterator<Item = &Self::Item> + '_> {
Box::new(useful::Iterable::iter(self))
}
// 通过不在上面使用默认函数体,
// 我们可以避免将迭代器装箱。
fn visit(&self, f: &mut dyn FnMut(&Self::Item)) {
for item in <Self as useful::Iterable>::iter(self) {
f(item)
}
}
}
}
|
我们还想将 erased::Iterable 传递给带有 useful::Iterable trait 约束的函数。然而,我们不能将其作为超 trait 添加,因为那会破坏 dyn 兼容性。
不过,erased::Iterable 的目的本来就是能类型擦除为 dyn erased::Iterable,因此我们直接在 dyn erased::Iterable 上实现 useful::Iterable:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| # fn main() {}
# pub mod useful {
# pub trait Iterable {
# type Item;
# type Iter<'a>: Iterator<Item = &'a Self::Item> where Self: 'a;
# fn iter(&self) -> Self::Iter<'_>;
# fn visit<F: FnMut(&Self::Item)>(&self, f: F);
# }
# }
pub mod erased {
use crate::useful;
# pub trait Iterable {
# type Item;
# fn iter(&self) -> Box<dyn Iterator<Item = &Self::Item> + '_>;
# fn visit(&self, f: &mut dyn FnMut(&Self::Item)) {
# for item in self.iter() {
# f(item);
# }
# }
# }
# impl<I: useful::Iterable + ?Sized> Iterable for I {
# type Item = <I as useful::Iterable>::Item;
# fn iter(&self) -> Box<dyn Iterator<Item = &Self::Item> + '_> {
# Box::new(useful::Iterable::iter(self))
# }
# fn visit(&self, f: &mut dyn FnMut(&Self::Item)) {
# for item in <Self as useful::Iterable>::iter(self) {
# f(item)
# }
# }
# }
impl<Item> useful::Iterable for dyn Iterable<Item = Item> + '_ {
type Item = Item;
type Iter<'a> = Box<dyn Iterator<Item = &'a Item> + 'a> where Self: 'a;
fn iter(&self) -> Self::Iter<'_> {
Iterable::iter(self)
}
// 这里可以选择重写默认函数体以避免将迭代器装箱,
// 或使用默认函数体以避免 `F` 的动态分发。我选择了前者。
fn visit<F: FnMut(&Self::Item)>(&self, mut f: F) {
<Self as Iterable>::visit(self, &mut f)
}
}
}
|
技术上,我们对 erased::Iterable 的 blanket 实现现在也适用于 dyn erased::Iterable,但由于一些语言魔法,一切仍能正常工作。
useful 模块中对 useful::Iterable 的 blanket 实现为我们提供了 &dyn erased::Iterable 和 Box<dyn erased::Iterable> 的实现,现在万事俱备!
审慎的实现及其局限
你可能注意到,我们尽可能避免将迭代器装箱,例如不为 erased::Iterable::visit 提供默认体,然后重写 useful::Iterable::visit 的默认体。这可以带来更好性能,但不一定关键,只要避免意外无限递归即可。
在这方面我们做得如何?让我们在 playground 中看看。
嗯,也许不如预期!<dyn erased::Iterable as useful::Iterable>::visit 按设计避免了装箱,但 Box<dyn erased::Iterable> 的 visit 仍会装箱迭代器。
为什么?因为 Box 的实现由 useful 模块提供,该实现使用默认体。要避免装箱,它需要递归到底层实现。
这样,visit 的调用会「向下钻取」直到 dyn erased::Iterable::visit 的实现,后者刻意避免装箱迭代器。换言之,递归实现会「尊重」其他 useful::Iterable 实现者对默认函数体的任何重写。
由于原始 trait 可能甚至不在你的 crate 中,这可能超出你的控制。唉,就这样吧;也许提交个 PR 🙂。在此特定情况下,你可以通过编写 &*boxed_erased_iterable 来注意传递 &dyn erased::Iterable。
或者对你的用例而言,也许根本不值得费心。
真实世界示例
也许使用此模式最流行的 crate 是 erased-serde 和 async-trait crate。处理 async 类 trait 总体上往往涉及大量类型擦除和不可命名类型。