如何在不知道其结构的情况下创建匹配枚举变体的宏?

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_typety 更改为identmatches! 之前添加use $enum_type::*;,这样您就可以删除@ 987654333@ 前缀。但是,如果您想进一步“简化”它,则需要一个 proc 宏,正如 Mihir 在现已删除的答案中提到的那样。 @vallentin,感谢您的编辑。虽然,由 proc-macro 生成的函数将被命名为is_Ais_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实例?

如何在不知道其表的情况下删除约束?

如何在不将控制器添加到视图层次结构并使其可见的情况下从 UIViewController 获取 UIView?

如何在不知道名称的情况下访问结构成员?

在迭代器中找到第一个特定的枚举变体并对其进行转换

如何对包装String的枚举变体进行模式匹配? [重复]