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

Alv*_*vra 9 enums rust rust-macros

我找到了以下解决方案来创建一个宏,该宏定义一个函数,如果枚举与变体匹配,该函数将返回 true:

macro_rules! is_variant {
    ($name: ident, $enum_type: ty, $enum_pattern: pat) => {
        fn $name(value: &$enum_type) -> bool {
            matches!(value, $enum_pattern)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

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);
Run Code Online (Sandbox Code Playgroud)

有没有办法定义这个宏,以避免为变体数据提供占位符?

换句话说,更改宏以便能够像这样使用它:

is_variant!(is_a, TestEnum, TestEnum::A);
is_variant!(is_a, TestEnum, TestEnum::B);
is_variant!(is_a, TestEnum, TestEnum::C);
Run Code Online (Sandbox Code Playgroud)

使用(如仅按变体而不是值比较枚举std::mem::discriminant中所述)没有帮助,因为它只能用于比较两个枚举实例。在这种情况下,只有一个对象和变体标识符。它还提到了匹配,但如果变体没有数据,则不起作用。TestEnum::A(..)

Mih*_*hir 10

您可以使用 proc 宏来做到这一点。Rust 书中有一章可能会有所帮助。

然后你可以像这样使用它:

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());
}
Run Code Online (Sandbox Code Playgroud)

对于上述效果,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 name = &input.ident;
    let 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 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)
}
Run Code Online (Sandbox Code Playgroud)

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"
Run Code Online (Sandbox Code Playgroud)

Cargo.toml对于二进制文件:

[dependencies]
is_variant_derive = { path = "../is_variant_derive" }
Run Code Online (Sandbox Code Playgroud)

然后将两个板条箱放在同一目录(工作空间)中,然后这样Cargo.toml

[workspace]
members = [
    "bin",
    "is_variant_derive",
]
Run Code Online (Sandbox Code Playgroud)

操场

另请注意,proc-macro 需要存在于其自己单独的 crate 中。


或者你可以直接使用is_variant crate。