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 总体上往往涉及大量类型擦除和不可命名类型。

最后修改 August 23, 2026: 更新 (499855b16)