如何以编程方式获取结构的字段数?

时间:2019-01-14 07:44:26

标签: struct rust rust-macros rust-proc-macros

我有一个自定义结构,如下所示:

struct MyStruct {
    first_field: i32,
    second_field: String,
    third_field: u16,
}

是否可以通过编程方式获取结构字段的数量(例如,通过方法调用field_count()):

let my_struct = MyStruct::new(10, "second_field", 4);
let field_count = my_struct.field_count(); // Expecting to get 3

对于此结构:

struct MyStruct2 {
    first_field: i32,
}

...以下调用应返回1

let my_struct_2 = MyStruct2::new(7);
let field_count = my_struct2.field_count(); // Expecting to get count 1

是否有像field_count()这样的API,还是只能通过宏来获取?

如果宏可以实现这一点,应该如何实现?

2 个答案:

答案 0 :(得分:19)

  

是否有像field_count()这样的API,还是只能通过宏来获取?

没有这样的内置API可以让您在运行时获取此信息。 Rust没有运行时反射(有关更多信息,请参见this question)。但是确实可以通过proc-macros!

注意:proc-macros与“通过示例宏”(通过macro_rules!声明)不同。后者不如proc-macros强大。

  

如果宏可以实现这一点,应该如何实现?

这不是proc-macros的简介;如果您对本主题完全陌生,请先阅读其他地方的简介。

在proc宏(例如,自定义派生)中,您将需要以某种方式获得结构定义为TokenStream。使用TokenStream和Rust语法的实际解决方案是通过syn对其进行解析:

#[proc_macro_derive(FieldCount)]
pub fn derive_field_count(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as ItemStruct);

    // ...
}

input的类型为ItemStruct。如您所见,它具有类型Fields的字段fields。在该字段上,您可以调用iter()以在结构的所有字段上获得迭代器,然后在其上可以调用count()

let field_count = input.fields.iter().count();

现在您有了想要的东西。

也许您想将此field_count()方法添加到您的类型中。您可以通过自定义派生(通过在此处使用quote板条箱)来做到这一点:

let name = &input.ident;

let output = quote! {
    impl #name {
        pub fn field_count() -> usize {
            #field_count
        }
    }
};

// Return output tokenstream
TokenStream::from(output)

然后,在您的应用程序中,您可以编写:

#[derive(FieldCount)]
struct MyStruct {
    first_field: i32,
    second_field: String,
    third_field: u16,
}

MyStruct::field_count(); // returns 3

答案 1 :(得分:7)

由宏生成结构本身是有可能的-在这种情况下,您可以只计算传递到宏中的令牌,如here所示。这就是我想出的:

macro_rules! gen {
    ($name:ident {$($field:ident : $t:ty),+}) => {
        struct $name { $($field: $t),+ }
        impl $name {
            fn field_count(&self) -> usize {
                gen!(@count $($field),+)
            }
        }
    };
    (@count $t1:tt, $($t:tt),+) => { 1 + gen!(@count $($t),+) };
    (@count $t:tt) => { 1 };
}

Playground(带有一些测试用例)

此方法的缺点(一个-可能会有更多)是在此函数中添加属性并非易事-例如,在其上#[derive(...)]上添加一些属性。另一种方法是编写自定义的派生宏,但这是我目前无法谈论的事情。