Rust过程宏入门(四)——遍历结构体字段
写于2021年1月16日。最早发表于知乎。
简单回顾
在上一章中的简单案例中,
#[derive(Builder)]
pub struct Command {
executable: String,
args: Vec<String>,
current_dir: String,
}
我们已经为Command
结构体生成了如下代码
impl Command {
pub fn builder() -> CommandBuilder {
CommandBuilder
}
}
pub struct CommandBuilder;
接下来,自然是补齐CommandBuilder
结构体的字段和成员函数,使其成为一个真正可用的类。
首先是CommandBuilder
类的字段,相对比较简单,只需要引用原结构体的字段名,并在其字段类型外面套上Option<>
即可。
获取结构体中的字段信息
如何获取原结构体的字段信息呢?这就需要用到上一章介绍的DeriveInput
了:
pub struct DeriveInput {
pub attrs: Vec<Attribute>,
pub vis: Visibility,
pub ident: Ident,
pub generics: Generics,
pub data: Data,
}
其中的data: Data
就是具体的结构体(或enum
、union
)信息。我们看Data
的定义,它也是一个enum
:
pub enum Data {
Struct(DataStruct),
Enum(DataEnum),
Union(DataUnion),
}
可见,这里的Data确实是支持struct
、enum
、union
三种结构的,但我们的Builder
宏只支持struct
,所以先忽略另外两个。 我们看DataStruct
的定义:
pub struct DataStruct {
pub struct_token: Struct,
pub fields: Fields,
pub semi_token: Option<Semi>,
}
它包含了一个struct
的Token,一些字段Fields
,以及一个可选的分号;
Token。继续看Fields
的定义:
pub enum Fields {
Named(FieldsNamed),
Unnamed(FieldsUnnamed),
Unit,
}
也是一个enum
。这是由于结构体中的字段有具名和匿名(类似元组Tuple)两种写法,还有一种不含任何字段的空结构体Fields::Unit
。Fields
类有一个iter
函数
impl Fields {
pub fn iter(&self) -> Iter<'_, Field> { /*...*/ }
}
可以生成其字段的迭代器,我们再看Field的定义:
pub struct Field {
pub attrs: Vec<Attribute>,
pub vis: Visibility,
pub ident: Option<Ident>,
pub colon_token: Option<Colon>,
pub ty: Type,
}
终于找到了我们所需要的信息!其中的ident: Option<Ident>
即是可选的字段名(但由于我们的Builder
类只支持具名结构体,所以字段名是必须的),ty: Type
即为类型信息。
遍历结构体字段
要遍历结构体的字段,首先需要将字段提取出来。前面已经介绍了DeriveInput
相关的结构,那么我们需要做的就是用match
或if let
的方式过滤出需要的信息。
设计只考虑具名的struct
类型,因此用if let
匹配单个分支即可:
if let Data::Struct(r#struct) = input.data {
let fields = r#struct.fields;
if matches!(&fields, Fields::Named(_)) {
todo!()
}
}
在todo!()
处则已经拿到了fields: Fields
,且它是具名的。至于不用if let
取出Fields::Named(_)
实体的原因是:即使用Named(FieldsNamed)
提取出的Field
结构体中的ident: Option<Ident>
也需要.unwrap()
之后才能取出Ident
,所以此处用原来的fields: Fields
即可。
现在用我们将Fields
映射为TokenStream2
,以便嵌入最终生成的代码中。
let builder_fields = TokenStream2::from_iter(
fields
.iter() // 1
.map(|field: &Field| (field.ident.as_ref().unwrap(), &field.ty)) // 2
.map(|(ident: &Ident, ty: &Type)| quote!(#ident: Option<#ty>, )), // 3
);
// 1
处的fields.iter()
产生了&Field
的迭代器,由于fields
可能被多次使用,所以用只读迭代器,而非按值传递将其消耗;// 2
处的.map(...)
提取出Field
中的标识符&Ident
与类型&Type
(由于前面保证了结构体字段是具名的,此处直接调用Option::unwrap
即可);// 3
处的.map(...)
根据提取出的ident
和ty
生成#ident: Option<#ty>,
的TokenStream
,注意不要漏掉末尾的Token。
最后,TokenStream2::from_iter
函数将以上的多个TokenStream2
串联在一起。对Command
类而言,将生成
executable: Option<String>,
args: Option<Vec<String>,
current_dir: Option<String>,
只需要将上述生成的字段嵌入CommandBuilder
结构体中,一个带字段的CommandBuilder
结构体便生成好了:
quote! {
pub struct CommandBuilder {
builder_fields
}
}
在为CommandBuilder
类添加字段后,Command::builder
函数中生成CommandBuilder
的默认实例还未添加字段初始值,这里简单起见,我们用#[derive(Default)]
派生宏自动为CommandBuilder
类实现Default
Trait,从而可以调用该Trait的default()
函数生成其默认实例。
quote! {
impl Command {
pub fn builder() -> CommandBuilder {
CommandBuilder::default()
}
}
#[derive(Default)]
pub struct CommandBuilder {
builder_fields
}
}
成员函数的生成
接下来,将为CommandBuilder
添加字段的设置函数。以executable
参数为例,期望通过以下函数设置其值:
impl CommandBuilder {
pub fn executable(mut self, value: String) -> Self {
self.executable = Some(executable);
self
}
}
这里的self
选择传值是因为希望CommandBuilder
类用完即毁,将其中所存的值直接转移到最终创建的Command
实例中,避免复制开销。返回一个Self
对象是为了实现链式调用。
显然,生成成员函数时也涉及原结构体字段的遍历,因此我们重用builder_fields
的生成函数,将其写成一个通用函数:
fn map_fields<F>(fields: &Fields, mapper: F) -> TokenStream2
where
F: FnMut((&Ident, &Type)) -> TokenStream2,
{
TokenStream2::from_iter(
fields
.iter()
.map(|field| (field.ident.as_ref().unwrap(), &field.ty))
.map(mapper),
)
}
因此即可用不同的映射函数或闭包以生成不同的TokenStream
了。
首先是设置字段的成员函数,相对较简单:
let builder_set_fields = map_fields(&fields, |(ident: &Field, ty: &Type)| {
quote!(pub fn #ident(mut self, value: #ty) -> Self {
self.#ident = Some(value);
self
})
});
基本上就是将前面手写的executable
函数中的字段名和类型替换成统一的#ident
和#ty
即可,非常方便。
最后是CommandBuilder::build
函数,它将消耗自身,并生成一个Result<Command, String>实例。当所有字段都已设置时,返回正常的Command
对象,否则返回错误信息(这里简单起见,暂时用String
)。
build
函数可以分为两部分:第一部分是依次取出由Option<>
包装过的CommandBuilder
类中的各个字段的值,第二部分是生成目标Command
实例。
先看第一部分,直接遍历原结构体中的字段即可:
let build_lets = map_fields(&fields, |(ident, _)| { // 忽略用不到的ty
quote!(
let #ident = self.#ident.ok_or(format!(
"field \"{}\" required, but not set yet.",
stringify!(#ident),
))?;
)
});
这里用到了Option::ok_or
方法,当其为None
时,返回一个错误值,然后用?运算符取出其中正确的值(若出现错误值,则直接将错误值返回上层)。
取出了构造结构体所需要的各字段值后,第二部分就变得相对简单了,直接返回一个Ok(Command{ executable, args, current_dir, })
即可。外层的字段名再用一次结构体字段遍历可得:
let build_values = map_fields(&fields, |(ident, _)| quote!(#ident,));
注意不要漏掉逗号,
。
最后将代码全部合到一起:
fn map_fields<F>(fields: &Fields, mapper: F) -> TokenStream2
where
F: FnMut((&Ident, &Type)) -> TokenStream2,
{
TokenStream2::from_iter(
fields
.iter()
.map(|field| (field.ident.as_ref().unwrap(), &field.ty))
.map(mapper),
)
}
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let ident = input.ident;
let ident_builder = Ident::new(&format!("{}Builder", ident), ident.span());
if let Data::Struct(r#struct) = input.data {
let fields = r#struct.fields;
if matches!(&fields, Fields::Named(_)) {
let builder_fields = map_fields(&fields, |(ident, ty)| quote!(#ident: Option<#ty>, ));
let builder_set_fields = map_fields(&fields, |(ident, ty)| {
quote!(pub fn #ident(mut self, value: #ty) -> Self {
self.#ident = Some(value);
self
})
});
let build_lets = map_fields(&fields, |(ident, _)| {
quote!(
let #ident = self.#ident.ok_or(format!(
"field \"{}\" required, but not set yet.",
stringify!(#ident),
))?;
)
});
let build_values = map_fields(&fields, |(ident, _)| quote!(#ident,));
quote!(
impl #ident {
pub fn builder() -> #ident_builder {
ident_builder::default()
}
}
#[derive(Default)]
pub struct #ident_builder {
builder_fields
}
impl #ident_builder {
builder_set_fields
pub fn build(self) -> Result<#ident, String> {
build_lets
Ok(#ident { #build_values })
}
}
)
.into()
}
}
quote!().into()
}
输出TokenStream2
以检验生成结果
上一章中讲到可以用cargo expand
命令来检验代码生成结果。但由于本文中用到了#[defive(Default)
派生宏以及format!
规则宏等,若用cargo expand
命令展开,会有一些不相干的信息,代码会很繁琐。但实际上除了cargo expand
之后,还可以直接输出TokenStream2
来检验生成结果。
方法很简单,直接将quote!
宏生成出的TokenStream2
用eprintln!
宏输出即可(eprintln!
的用法与println!
相似,只不过是输出到标准错误stderr
中):
let tokens = quote!( /* ... */ );
eprintln!("{}", tokens);
输出后的代码未经格式化,我们可以手动复制到一个空白文件然后使用rustfmt
程序格式化,得
impl Command {
pub fn builder() -> CommandBuilder {
CommandBuilder::default()
}
}
#[derive(Default)]
pub struct CommandBuilder {
executable: Option<String>,
args: Option<Vec<String>>,
current_dir: Option<String>,
}
impl CommandBuilder {
pub fn executable(mut self, value: String) -> Self {
self.executable = Some(value);
self
}
pub fn args(mut self, value: Vec<String>) -> Self {
self.args = Some(value);
self
}
pub fn current_dir(mut self, value: String) -> Self {
self.current_dir = Some(value);
self
}
pub fn build(self) -> Result<Command, String> {
let executable = self.executable.ok_or(format!(
"field \"{}\" required, but not set yet.",
stringify!(executable),
))?;
let args = self.args.ok_or(format!(
"field \"{}\" required, but not set yet.",
stringify!(args),
))?;
let current_dir = self.current_dir.ok_or(format!(
"field \"{}\" required, but not set yet.",
stringify!(current_dir),
))?;
Ok(Command {
executable,
args,
current_dir,
})
}
}
这种方法在调用派生宏中出现编译错误时尤其有用,不仅可以用来检验整体代码片段,也可以随时查看某一小片段,便于调试。
功能验证
最后,为了验证派生宏的功能正确,将编译运行以下程序:
use derive_builder::Builder;
#[derive(Builder)]
pub struct Command {
executable: String,
args: Vec<String>,
current_dir: String,
}
fn main() {
let command = Command::builder()
.executable("cargo".to_owned())
.args(vec!["build".to_owned(), "--release".to_owned()])
.current_dir("..".to_owned())
.build()
.unwrap();
assert_eq!(command.executable, "cargo");
assert_eq!(command.args, &["build", "--release"]);
assert_eq!(command.current_dir, "..");
}
程序将正常执行结束,无任何输出。
本期的介绍到此为止,主要介绍了如何遍历Command
结构体中的字段信息,并生成CommandBuilder
类的字段与成员函数,实现了一个最简单可用的Builder
类。接下来将继续如何处理原结构体中的可选、多选字段等功能。
本文中的代码实现详见 https://github.com/frank-king/proc-macro-workshop/tree/example/builder/05-method-chaining.rs/builder。
其中,测试用的main.rs
代码见example/builder/05-method-chaining.rs
中的main.rs
文件。