#sealed #convert #cast #any #from

struct-variant

生成枚举的最小辅助宏,从结构体列表中生成枚举

1 个稳定版本

1.0.2 2021 年 5 月 15 日

#2777Rust 模式

每月 24 次下载

MIT 许可证

16KB
261

Crates Docs License Build

生成枚举的最小辅助宏,从结构体列表中生成枚举。

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 关键字进行向下转换。这种方法的缺点是双重的

  1. 每个变体都实现了 Shape,那么为什么 ShapeEnum 不实现 Shape 的所有方法呢?
  2. 每个结构体只有一个枚举区分符,那么为什么不能自动将任何 Shape 转换为 ShapeEnum 呢?
  3. 还有额外的模板代码。我们可以通过实现 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 的实现:CircleRectangle。在其它语言中,一个实现了所有子类的基础类型的类型被称为 密封类型。虽然这个库不能保证你在枚举中包含所有子类型,但通过尽职调查,你可以编写出确实这样做的库。密封特质设计模式 确保了你的库的用户不能添加新的子类型,因此结合这个库,你可以确保你正在创建密封类型。

特质边界

早期示例创建了一个基于 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,
}

名称冲突

这个库的一个争议来源是与具有冲突名称的结构体。假设我们同时在 barbaz 命名空间中都有两个不同的 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