1 个稳定版本
1.0.2 | 2021 年 5 月 15 日 |
---|
#2777 在 Rust 模式
每月 24 次下载
16KB
261 行
生成枚举的最小辅助宏,从结构体列表中生成枚举。
60 秒示例
安装
cargo add struct-variant
设置
// Common trait.
trait Event {
fn apply(&self);
};
// First trait implementation.
struct MouseEvent {
// ...
}
impl Event for MouseEvent {
fn apply(&self) {
println!("Applied mouse");
}
}
// Second trait implementation.
struct KeyboardEvent {
// ...
}
impl Event for KeyboardEvent {
fn apply(&self) {
println!("Applied keyboard");
}
}
// The *magic*!
#[struct_variant(Event)]
enum EventEnum {
MouseEvent,
KeyboardEvent,
}
结果
fn process_event(event: EventEnum) {
// No need to match to get inner type.
// AsRef<Event> implemented for you.
event.as_ref().apply();
// But you can still match if you want to.
match event {
EventEnum::MouseEvent(_) => println!("Got mouse"),
EventEnum::KeyboardEvent(_) => println!("Got keyboard"),
}
}
fn main() {
// From<MouseEvent> for EventEnum implemented for you.
let mouse_event = MouseEvent {};
process_event(mouse_event.into())
}
详细动机
假设你有一个 trait Shape
,它被有限数量的结构体实现
trait Shape {
fn area(&self) -> usize;
}
struct Circle { ... }
struct Rectangle { ... };
偶尔我们可能想传递一个具有向下转换能力的动态 Shape
类型。偶尔可以使用 std::any::Any
,但它并不适用于所有场景(见官方 Rust 文档中的限制)。我们可以传递一个 &dyn Shape
,但你只能调用公共 trait 方法。为了解决这两个问题,我们可以创建一个包含所有变体的枚举
enum ShapeEnum {
Circle(Circle),
Rectangle(Rectangle),
}
现在,我们不再传递 dyn Shape
,而是传递 ShapeEnum
并使用 match
关键字进行向下转换。这种方法的缺点是双重的
- 每个变体都实现了
Shape
,那么为什么ShapeEnum
不实现Shape
的所有方法呢? - 每个结构体只有一个枚举区分符,那么为什么不能自动将任何
Shape
转换为ShapeEnum
呢? - 还有额外的模板代码。我们可以通过实现 trait 来解决 #1 和 #2,但这需要大量的额外代码。
这个库帮助解决了所有这些问题
#[struct_variant(Shape)]
enum ShapeEnum {
Circle,
Rectangle,
}
宏属性中的 Shape
指示枚举中的所有结构体都实现了 Shape
。你可以使用 std::convert::AsRef
来将你的类型转换为宏属性中列出的任何类型。这解决了 #1 问题。现在我们可以更方便地使用它
fn print_shape(shape: ShapeEnum) {
// We can use pattern matching to downcast.
let name = match shape {
ShapeEnum::Circle(_) => "Circle",
ShapeEnum::Rectangle(_) => "Rectangle",
};
// AsRef<dyn Shape> is implemented for you.
println!("Shape: {}, Area: {}", name, shape.as_ref().area());
}
std::convert::From
对每个结构体都进行了实现,因此你也可以进行向上转换。这解决了 #2 问题
fn print_area(shape: &dyn Shape) {
println!("Area: {}", shape.area());
}
let circle: ShapeEnum = Circle::with_radius(2).into();
print_area(circle.as_ref());
密封类型
在早期示例中,ShapeEnum
包含了两个 Shape
的实现:Circle
和 Rectangle
。在其它语言中,一个实现了所有子类的基础类型的类型被称为 密封类型。虽然这个库不能保证你在枚举中包含所有子类型,但通过尽职调查,你可以编写出确实这样做的库。密封特质设计模式 确保了你的库的用户不能添加新的子类型,因此结合这个库,你可以确保你正在创建密封类型。
特质边界
早期示例创建了一个基于 Shape
特质的 ShapeEnum
类型。宏属性允许多个特质边界
#[struct_variant(Shape + Debug)]
enum ShapeEnumWithDebug {
Circle,
Rectangle,
}
fn print_debug(debug: &dyn Debug) {
println!("Debug: {:?}", debug);
}
let circle: ShapeEnumWithDebug = Circle::with_radius(2).into();
print_debug(circle.as_ref());
println!("Manual: {:?}", AsRef::<dyn Debug>::as_ref(&circle));
或者你也可以选择没有任何特质边界。这意味着你将没有 std::convert::AsRef
实现,但你仍然可以受益于 std::convert::From
实现
#[struct_variant]
enum ShapeEnumNoBounds {
Circle,
Rectangle,
}
名称冲突
这个库的一个争议来源是与具有冲突名称的结构体。假设我们同时在 bar
和 baz
命名空间中都有两个不同的 Foo
。使用这个库的一个简单方法是在导入它们之一或两个时使用 as
use bar::Foo as FooBar;
use baz::Foo;
#[struct_variant]
enum Qux {
FooBar,
Foo
}
另一种方法是使用标准枚举语法来声明它使用哪个结构体
use baz::Foo;
#[struct_variant]
enum Qux {
FooBar(bar::Foo),
Foo
}
这两种方法的缺点是,在模式匹配时你必须使用重新导出的名称。
我们可以用同样的方法来支持同一泛型类型的多个变体
struct Foo<X> {
phantom: PhantomType<X>,
};
enum Qux {
FooU8(Foo<u8>),
FooI8(Foo<i8>),
}
贡献
欢迎并非常感谢贡献!
在提交拉取请求之前,请先运行格式化程序、代码检查器和单元测试
cargo +nightly fmt
cargo clippy
cargo test
构建文档
这个库使用一个不稳定特性从 README 文件构建文档
cargo +nightly doc --open --features doc
依赖关系
~2MB
~44K SLoC