26个版本
0.3.13 | 2024年3月28日 |
---|---|
0.3.12 | 2023年7月6日 |
0.3.11 | 2023年1月12日 |
0.3.9 | 2022年12月29日 |
0.1.2 | 2018年12月22日 |
#17 in 进程宏
586,304 每月下载量
用于 682 个crate(189 直接使用)
72KB
878 行
enum_dispatch
enum_dispatch
将您的trait对象转换为具体的复合类型,提高其方法调用速度最多10倍。
示例
如果您有以下代码...
// We already have defined MyImplementorA and MyImplementorB, and implemented MyBehavior for each.
trait MyBehavior {
fn my_trait_method(&self);
}
// Any pointer type -- Box, &, etc.
let a: Box<dyn MyBehavior> = Box::new(MyImplementorA::new());
a.my_trait_method(); //dynamic dispatch
...则可以使用enum_dispatch
进行如下改进
#[enum_dispatch]
enum MyBehaviorEnum {
MyImplementorA,
MyImplementorB,
}
#[enum_dispatch(MyBehaviorEnum)]
trait MyBehavior {
fn my_trait_method(&self);
}
let a: MyBehaviorEnum = MyImplementorA::new().into();
a.my_trait_method(); //no dynamic dispatch
注意区别
- 新的枚举
MyBehaviorEnum
,其变体是简单地实现traitMyBehavior
的类型。 - 应用于枚举和trait的新
enum_dispatch
属性,将它们连接起来。 - 移除了
Box
分配。 - 更快的trait方法调用!
如何使用
- 将
enum_dispatch
添加为Cargo.toml依赖项,并在您的代码中使用use enum_dispatch::enum_dispatch
。 - 创建一个新枚举,其变体是您定义的任何作用域内的trait实现者。
- 将
#[enum_dispatch]
属性添加到枚举或trait定义中。这将将其“注册”到enum_dispatch
库中。请注意应用到的枚举或trait的名称——我们将称之为FirstBlockName
。 - 将
#[enum_dispatch(FirstBlockName)]
属性添加到剩余的定义中。这将将其“链接”到先前注册的定义。 - 更新您的动态类型,以使用新的枚举类型。您可以使用来自任何特性行实现者的
.into()
来自动将其转换为枚举变体。
性能
关于性能的更多信息可以在文档中找到,并且基准测试在 benches
目录中可用。以下基准测试结果展示了使用 enum_dispatch
可以达到的效果。它们比较了使用 Box
封装、引用或 enum_dispatch
枚举类型在 1024 个具有随机具体类型的特性行对象向量上重复访问方法调用速度。
test benches::boxdyn_homogeneous_vec ... bench: 5,900,191 ns/iter (+/- 95,169)
test benches::refdyn_homogeneous_vec ... bench: 5,658,461 ns/iter (+/- 137,128)
test benches::enumdispatch_homogeneous_vec ... bench: 479,630 ns/iter (+/- 3,531)
附加功能
序列化兼容性
尽管 enum_dispatch
是在考虑性能的前提下构建的,但它所应用的转换使得您所有的数据结构对编译器都变得更加可见。这意味着您可以在特性行对象上使用 serde
或其他类似工具!
自动 From
和 TryInto
实现
enum_dispatch
将为所有内部类型生成一个 From
实现以便于实例化您自定义的枚举。此外,它还将为所有内部类型生成一个 TryInto
实现以便于转换回原始的未封装类型。
属性支持
您可以在 enum_dispatch
变体上使用 #[cfg(...)]
属性来有条件地包含或排除相应的 enum_dispatch
实现。其他属性将直接传递到底层的生成枚举,从而与其他过程宏兼容。
no_std
支持
enum_dispatch
在 no_std
环境中得到支持。它非常适合嵌入式设备,在这些设备上,能够在栈上分配特性行对象集合非常有用。
调整和选项
自定义变体名称
默认情况下,enum_dispatch
将将每个枚举变体扩展为具有与内部类型同名的单个未命名字段的枚举。如果您在某些情况下希望为 enum_dispatch
变体中的特定类型使用自定义名称,可以按照以下方式操作
#[enum_dispatch]
enum MyTypes {
TypeA,
CustomVariantName(TypeB),
}
let mt: MyTypes = TypeB::new().into();
match mt {
TypeA(a) => { /* `a` is a TypeA */ },
CustomVariantName(b) => { /* `b` is a TypeB */ },
}
枚举和特性行为的泛型参数需要自定义变体名称,这也可以通过 enum_dispatch
进行优化。查看 此泛型示例 以了解其工作方式。
一次性指定多个枚举
如果您想使用 enum_dispatch
为多个枚举实现相同的特性行为,您可以在同一个属性中指定它们。
#[enum_dispatch(Widgets, Tools, Gadgets)]
trait CommonFunctionality {
// ...
}
一次性指定多个特性行为
类似于上面,您可以使用单个属性为单个枚举实现多个特性行为。
#[enum_dispatch(CommonFunctionality, WidgetFunctionality)]
enum Widget {
// ...
}
泛型枚举和特性行为
enum_dispatch
可以操作带有泛型参数的枚举和特质。在链接时,请确保在属性参数中包含泛型参数,如下所示
#[enum_dispatch]
trait Foo<T, U> { /* ... */ }
#[enum_dispatch(Foo<T, U>)]
enum Bar<T: Clone, U: Hash> { /* ... */ }
枚举和特质中对应泛型参数的名称应匹配。
这个例子更详细地演示了这一点。
故障排除
没有创建实现?
请注意不要忘记属性或在一个链接属性中误拼名称。如果在找到链接属性之前解析完成,则不会生成实现。由于宏系统的技术限制,在这种情况下无法正确警告用户。
无法解析枚举?
类型必须完全在作用域内才能用作枚举变体。例如,以下代码将无法编译
#[enum_dispatch]
enum Fails {
crate::A::TypeA,
crate::B::TypeB,
}
这是因为枚举必须在宏展开之前正确解析。相反,首先导入类型
use crate::A::TypeA;
use crate::B::TypeB;
#[enum_dispatch]
enum Succeeds {
TypeA,
TypeB,
}
技术细节
enum_dispatch
是一个过程宏,以枚举的形式实现了一组固定类型的特质。这比使用动态调度更快,因为类型信息将“内置”到每个枚举中,避免了昂贵的vtable查找。
由于 enum_dispatch
是一个过程宏,它通过在编译时处理和展开属性代码来工作。以下部分解释了上述示例可能如何被转换。
枚举处理
无法定义其变体是实际具体类型的枚举。为了解决这个问题,enum_dispatch
通过为每个变体生成一个名称并使用提供的类型作为其单一元组样式参数来重写其主体。对于大多数目的来说,每个变体的名称并不特别重要,但enum_dispatch
目前将使用提供的类型的名称。
enum MyBehaviorEnum {
MyImplementorA(MyImplementorA),
MyImplementorB(MyImplementorB),
}
特质处理
enum_dispatch
实际上并不处理注解特质!然而,它仍然需要访问特质定义,以便它可以记录特质的名称以及它内部任何方法的函数签名。
特质实现创建
每当 enum_dispatch
能够“链接”两个定义时,它将生成一个 impl
块,为枚举实现特质。在上面的例子中,链接是通过 MyBehavior
特质定义完成的,因此将直接在该特质下方生成 impl
块。生成的实现块可能看起来像这样
impl MyBehavior for MyBehaviorEnum {
fn my_trait_method(&self) {
match self {
MyImplementorA(inner) => inner.my_trait_method(),
MyImplementorB(inner) => inner.my_trait_method(),
}
}
}
将相应地展开额外的特质方法,并且额外的枚举变体将与每个方法定义中的额外匹配分支相对应。很容易看出,手动编写的代码如何迅速变得难以管理!
'From' 实现创建
通常,在不了解其名称的情况下初始化新的枚举变体是不可能的。然而,通过为每个变体实现 From<T>
,这个要求得到了缓解。生成的实现可能看起来像以下这样
impl From<MyImplementorA> for MyBehaviorEnum {
fn from(inner: MyImplementorA) -> MyBehaviorEnum {
MyBehaviorEnum::MyImplementorA(inner)
}
}
impl From<MyImplementorB> for MyBehaviorEnum {
fn from(inner: MyImplementorB) -> MyBehaviorEnum {
MyBehaviorEnum::MyImplementorB(inner)
}
}
与上面一样,大量可能的类型变体会使手动维护变得非常困难。
注册和链接
任何熟悉编写宏的人都知道,宏必须在本地处理,没有任何关于周围源代码的上下文。此外,在syn
中解析的语法项是!Send
和!Sync
。这有一个很好的原因——在多线程编译和宏展开中,任何给定代码块的引用的顺序或生命周期都没有保证。不幸的是,这也阻止了在不同宏调用之间引用语法。
为了方便起见,enum_dispatch
通过将这些语法转换为String
并将它们存储在懒加载的once_cell
中绕过了这些限制,这些懒加载的Mutex<HashMap<String, String>>
的键是特质或枚举名称。
还有一个类似的HashMap
专门用于“延迟”链接,因为不同文件中的定义可能会以任意顺序出现。如果一个链接属性(有一个参数)在相应的注册属性(没有参数)之前出现,该参数将被存储为延迟链接。一旦遇到该参数的定义,就可以像平常一样创建impl块。
由于链接延迟机制,在无法实现链接属性的情况下遇到链接属性不是错误。enum_dispatch
将期望在解析过程中稍后找到相应的注册属性。然而,没有办法在解析完所有原始源代码后插入回调来检查所有延迟链接都已处理,这就是为什么无法警告用户存在未链接属性的原因。
依赖关系
~315–790KB
~18K SLoC