如何在不知道其结构的情况下创建匹配枚举变体的宏?
Posted
技术标签:
【中文标题】如何在不知道其结构的情况下创建匹配枚举变体的宏?【英文标题】:How to create a macro that matches enum variants without knowing its structure? 【发布时间】:2021-03-18 19:05:32 【问题描述】:我找到了以下解决方案来创建一个宏,该宏定义一个函数,如果枚举与变体匹配,则返回 true:
macro_rules! is_variant
($name: ident, $enum_type: ty, $enum_pattern: pat) =>
fn $name(value: &$enum_type) -> bool
matches!(value, $enum_pattern)
用法:
enum TestEnum
A,
B(),
C(i32, i32),
is_variant!(is_a, TestEnum, TestEnum::A);
is_variant!(is_b, TestEnum, TestEnum::B());
is_variant!(is_c, TestEnum, TestEnum::C(_, _));
assert_eq!(is_a(&TestEnum::A), true);
assert_eq!(is_a(&TestEnum::B()), false);
assert_eq!(is_a(&TestEnum::C(1, 1)), false);
有没有办法定义这个宏,这样 可以避免为变体数据提供占位符吗?
换句话说,将宏更改为能够像这样使用它:
is_variant!(is_a, TestEnum, TestEnum::A);
is_variant!(is_a, TestEnum, TestEnum::B);
is_variant!(is_a, TestEnum, TestEnum::C);
使用std::mem::discriminant
(如Compare enums only by variant, not value 中所述)无济于事,因为它只能用于比较两个枚举实例。在这种情况下,只有一个对象和变体标识符。
它还提到了 TestEnum::A(..)
上的匹配,但如果变体没有数据,这将不起作用。
【问题讨论】:
如果您将其移动到impl TestEnum ...
内,则将$enum_type
从ty
更改为ident
在matches!
之前添加use $enum_type::*;
,这样您就可以删除@ 987654333@ 前缀。但是,如果您想进一步“简化”它,则需要一个 proc 宏,正如 Mihir 在现已删除的答案中提到的那样。
@vallentin,感谢您的编辑。虽然,由 proc-macro 生成的函数将被命名为is_A
、is_B
等。它使用变体的名称来给函数命名。可悲的是,我不知道任何将其转换为小写的方法。
@Mihir 我刚刚更新了你的答案,所以现在它们被转换为小写:)
@vallentin,啊,没注意到,我会恢复我的编辑。谢谢:)
【参考方案1】:
您可以使用 proc 宏来做到这一点。有一个chapter in rust book 可能会有所帮助。
然后你可以像这样使用它:
use is_variant_derive::IsVariant;
#[derive(IsVariant)]
enum TestEnum
A,
B(),
C(i32, i32),
D _name: String, _age: i32 ,
fn main()
let x = TestEnum::C(1, 2);
assert!(x.is_c());
let x = TestEnum::A;
assert!(x.is_a());
let x = TestEnum::B();
assert!(x.is_b());
let x = TestEnum::D _name: "Jane Doe".into(), _age: 30 ;
assert!(x.is_d());
对于上述效果,proc 宏 crate 将如下所示:
is_variant_derive/src/lib.rs
:
extern crate proc_macro;
use proc_macro::TokenStream;
use proc_macro2::Span, TokenStream as TokenStream2;
use quote::format_ident, quote, quote_spanned;
use syn::spanned::Spanned;
use syn::parse_macro_input, Data, DeriveInput, Error, Fields;
// https://crates.io/crates/convert_case
use convert_case::Case, Casing;
macro_rules! derive_error
($string: tt) =>
Error::new(Span::call_site(), $string)
.to_compile_error()
.into();
;
#[proc_macro_derive(IsVariant)]
pub fn derive_is_variant(input: TokenStream) -> TokenStream
// See https://doc.servo.org/syn/derive/struct.DeriveInput.html
let input: DeriveInput = parse_macro_input!(input as DeriveInput);
// get enum name
let ref name = input.ident;
let ref data = input.data;
let mut variant_checker_functions;
// data is of type syn::Data
// See https://doc.servo.org/syn/enum.Data.html
match data
// Only if data is an enum, we do parsing
Data::Enum(data_enum) =>
// data_enum is of type syn::DataEnum
// https://doc.servo.org/syn/struct.DataEnum.html
variant_checker_functions = TokenStream2::new();
// Iterate over enum variants
// `variants` if of type `Punctuated` which implements IntoIterator
//
// https://doc.servo.org/syn/punctuated/struct.Punctuated.html
// https://doc.servo.org/syn/struct.Variant.html
for variant in &data_enum.variants
// Variant's name
let ref variant_name = variant.ident;
// Variant can have unnamed fields like `Variant(i32, i64)`
// Variant can have named fields like `Variant x: i32, y: i32`
// Variant can be named Unit like `Variant`
let fields_in_variant = match &variant.fields
Fields::Unnamed(_) => quote_spanned! variant.span()=> (..) ,
Fields::Unit => quote_spanned! variant.span()=> ,
Fields::Named(_) => quote_spanned! variant.span()=> .. ,
;
// construct an identifier named is_<variant_name> for function name
// We convert it to snake case using `to_case(Case::Snake)`
// For example, if variant is `HelloWorld`, it will generate `is_hello_world`
let mut is_variant_func_name =
format_ident!("is_", variant_name.to_string().to_case(Case::Snake));
is_variant_func_name.set_span(variant_name.span());
// Here we construct the function for the current variant
variant_checker_functions.extend(quote_spanned! variant.span()=>
fn #is_variant_func_name(&self) -> bool
match self
#name::#variant_name #fields_in_variant => true,
_ => false,
);
// Above we are making a TokenStream using extend()
// This is because TokenStream is an Iterator,
// so we can keep extending it.
//
// proc_macro2::TokenStream:- https://docs.rs/proc-macro2/1.0.24/proc_macro2/struct.TokenStream.html
// Read about
// quote:- https://docs.rs/quote/1.0.7/quote/
// quote_spanned:- https://docs.rs/quote/1.0.7/quote/macro.quote_spanned.html
// spans:- https://docs.rs/syn/1.0.54/syn/spanned/index.html
_ => return derive_error!("IsVariant is only implemented for enums"),
;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let expanded = quote!
impl #impl_generics #name #ty_generics #where_clause
// variant_checker_functions gets replaced by all the functions
// that were constructed above
#variant_checker_functions
;
TokenStream::from(expanded)
Cargo.toml
用于名为 is_variant_derive
的库:
[lib]
proc-macro = true
[dependencies]
syn = "1.0"
quote = "1.0"
proc-macro2 = "1.0"
convert_case = "0.4.0"
Cargo.toml
表示二进制文件:
[dependencies]
is_variant_derive = path = "../is_variant_derive"
然后将两个 crate 放在同一个目录(工作区)中,然后有这个 Cargo.toml
:
[workspace]
members = [
"bin",
"is_variant_derive",
]
Playground
还要注意 proc-macro 需要存在于它自己单独的 crate 中。
或者你可以直接使用is_variant crate。
【讨论】:
我应该更明确地说,宏应该创建一个执行检查的函数,作为更大宏的一部分。我会更新问题。 感谢您的详尽回答。你是说你需要一个令牌流来获取变体字段来解决这个问题吗?宏用于为单独的结构创建方法,而不是枚举本身。 @Alvra 宏通常在句法级别上工作,因此您无法仅通过传递TestEnum
和/或 TestEnum::A
获得所需的信息。为了能够获取该信息,您需要用例如包装整个枚举。 macro_rules! impl_enum
。但是,当您尝试扩展和生成这些函数时会遇到问题。
@Alvra,macro_rules
你不知道枚举字段。 derive
类型宏为您提供有关 enum/struct/union 中字段的信息。使用它,您可以生成任何代码。它不一定限于为枚举本身生成方法。如果需要,您还可以生成自定义函数。我将在上面的代码中添加一些 cmets 使其更清晰。
我已经用 cmets 更新了代码。如果您按顺序阅读所有链接,编写自己的自定义 proc-macro 会变得更容易。以上是关于如何在不知道其结构的情况下创建匹配枚举变体的宏?的主要内容,如果未能解决你的问题,请参考以下文章
如何在不物理创建结构的情况下获得结构的reflect.Type实例?