Rust builder 过程宏
Rust Builder Pattern Procedural Macro
本文档详细解释了如何使用 Rust 的过程宏(procedural macro)来实现构建者模式(Builder Pattern)。这个实现类似于 derive_builder
crate 的功能,支持常规设置方法、链式调用以及处理重复字段。
设计目标
这个 Builder 宏旨在实现以下功能:
- 为结构体自动生成一个对应的 Builder 结构体
- 支持常规的 setter 方法和链式调用
- 处理
Vec
类型的重复字段,提供单项添加方法 - 对必填字段进行验证,仅允许在所有必填字段都设置后构建
- 为
Option
类型字段提供特殊处理,使其成为可选字段
直接上完整项目结构
代码解析请看后续
macrobuilder
├── Cargo.lock
├── Cargo.toml
├── derive_builder
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── README.md
└── testbuilder
├── Cargo.toml
└── src
└── main.rs
macrobuilder
这是个 workspace
./Cargo.toml
[workspace]
resolver = "3"
members = ["derive_builder", "testbuilder"]
derive_builder
./derive_builder/Cargo.toml
[package]
name = "derive_builder"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
darling = "0.20.11"
proc-macro2 = "1.0.95"
quote = "1.0.40"
syn = { version = "2.0.101", features = ["full", "extra-traits"] }
./derive_builder/src/lib.rs
use darling::{FromDeriveInput, FromField};
use proc_macro::TokenStream;
use quote::{ToTokens, quote};
use syn::{DeriveInput, Ident, Type};
/// 定义从字段提取的属性信息结构
#[derive(Debug, FromField)]
#[darling(attributes(builder))]
struct BuilderFieldOptions {
/// 是否跳过该字段
#[darling(default)]
skip: bool,
/// "each" 属性,用于处理 Vec 类型的重复字段
/// 如 #[builder(each = "arg")] args: Vec<String>
#[darling(default)]
each: Option<String>,
}
/// 定义从派生输入提取的属性信息结构
#[allow(unused)]
#[derive(Debug, FromDeriveInput)]
#[darling(attributes(builder), supports(struct_named))]
struct BuilderOptions {
ident: Ident,
data: darling::ast::Data<(), BuilderField>,
}
/// 表示构建器中的一个字段
#[allow(unused)]
#[derive(Debug, FromField)]
#[darling(attributes(builder))]
struct BuilderField {
ident: Option<Ident>,
ty: Type,
#[darling(default)]
skip: bool,
#[darling(default)]
each: Option<String>,
}
/// 主要的派生宏入口点
#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive_builder(input: TokenStream) -> TokenStream {
// 解析输入的 TokenStream 为 DeriveInput
let input = syn::parse_macro_input!(input as DeriveInput);
// 提取结构体名称和字段
let name = &input.ident;
let builder_name = quote::format_ident!("{}Builder", name);
// 从输入中提取字段
let fields = match &input.data {
syn::Data::Struct(data) => match &data.fields {
syn::Fields::Named(fields) => &fields.named,
_ => panic!("Builder 只支持命名字段的结构体"),
},
_ => panic!("Builder 只支持结构体"),
};
// 处理每个字段,生成对应的构建器字段、setter 方法等
let mut builder_fields = std::vec::Vec::new();
let mut builder_init = std::vec::Vec::new();
let mut builder_setters = std::vec::Vec::new();
let mut builder_build_fields = std::vec::Vec::new();
for field in fields {
// 获取字段名称和类型
let field_name = field.ident.as_ref().unwrap();
let field_type = &field.ty;
// 解析字段上的 #[builder(...)] 属性
let options = match BuilderFieldOptions::from_field(field) {
std::result::Result::Ok(options) => options,
std::result::Result::Err(e) => panic!("解析字段属性失败:{}", e),
};
// 如果设置了 skip,则跳过该字段
if options.skip {
continue;
}
// 检查是否是 Vec 类型且有 each 属性
let is_vec = if let syn::Type::Path(type_path) = &field_type {
let last_segment = type_path.path.segments.last().unwrap();
last_segment.ident == "Vec"
} else {
false
};
// 确定构建器中字段的类型
let builder_field_type = if is_option_type(field_type) {
// 如果是 Option<T>,构建器中的类型也是 Option<T>
quote! { #field_type }
} else {
// 否则,构建器中的类型是 Option<T>
quote! { std::option::Option<#field_type> }
};
// 添加构建器字段
builder_fields.push(quote! {
#field_name: #builder_field_type
});
// 初始化构建器字段为 None
builder_init.push(quote! {
#field_name: std::option::Option::None
});
let setter_value_type = if is_option_type(field_type) {
extract_option_inner_type(field_type)
} else {
quote! { #field_type }
};
// 为字段生成 setter 方法
let regular_setter = quote! {
// 常规 setter 方法,允许直接设置整个字段值
pub fn #field_name(&mut self, #field_name: #setter_value_type) -> &mut Self {
self.#field_name = std::option::Option::Some(#field_name);
self
}
};
builder_setters.push(regular_setter);
// 如果是 Vec 类型且有 each 属性,添加单项添加方法
if is_vec && options.each.is_some() {
let each_name =
proc_macro2::Ident::new(&options.each.unwrap(), proc_macro2::Span::call_site());
// 如果 each_name 和 field_name 是不一样的,那么就生成 each 的方法
if each_name.to_string() != field_name.to_string() {
// 提取 Vec 的内部类型
let inner_type = extract_vec_inner_type(field_type);
// 添加单项添加方法
let each_setter = quote! {
// 单项添加方法,用于向 Vec 中添加单个元素
pub fn #each_name(&mut self, item: #inner_type) -> &mut Self {
if self.#field_name.is_none() {
self.#field_name = std::option::Option::Some(std::vec::Vec::new());
}
self.#field_name.as_mut().unwrap().push(item);
self
}
};
builder_setters.push(each_setter);
}
}
// 生成构建时的字段处理逻辑
if is_option_type(field_type) {
// 对于 Option 类型,如果 builder 中是 None 就保持 None
builder_build_fields.push(quote! {
#field_name: self.#field_name.clone()
});
} else {
// 对于非 Option 类型,如果 builder 中是 None 就返回错误
builder_build_fields.push(quote! {
// ok_or(concat!(stringify!(#field_name), " is not set"))
#field_name: self.#field_name.clone().unwrap_or_default()
});
}
}
// 生成最终的构建器代码
let expanded = quote! {
// 定义构建器结构体
pub struct #builder_name {
#(#builder_fields,)*
}
// 为主结构体实现 builder 方法
impl #name {
pub fn builder() -> #builder_name {
#builder_name {
#(#builder_init,)*
}
}
}
// 为构建器实现各种方法
impl #builder_name {
// 生成 setter 方法
#(#builder_setters)*
// 构建方法,创建最终的结构体实例
pub fn build(&self) -> std::result::Result<#name, std::string::String> {
std::result::Result::Ok(#name {
#(#builder_build_fields,)*
})
}
}
};
expanded.into()
}
/// 检查类型是否为 Option<T>
fn is_option_type(ty: &syn::Type) -> std::primitive::bool {
if let syn::Type::Path(type_path) = ty {
if let std::option::Option::Some(segment) = type_path.path.segments.last() {
return segment.ident == "Option";
}
}
false
}
/// 从 Vec<T> 中提取 T 的类型
fn extract_vec_inner_type(ty: &syn::Type) -> proc_macro2::TokenStream {
extract_generic_inner_type(ty, "Vec")
}
/// 从 Option<T> 中提取 T 的类型
fn extract_option_inner_type(ty: &syn::Type) -> proc_macro2::TokenStream {
extract_generic_inner_type(ty, "Option")
}
/// 从 Generic<T> 中提取 T 的类型
fn extract_generic_inner_type(
ty: &syn::Type,
generic_name: &std::primitive::str,
) -> proc_macro2::TokenStream {
if let syn::Type::Path(type_path) = ty {
if let std::option::Option::Some(segment) = type_path.path.segments.last() {
if segment.ident == generic_name {
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
if let std::option::Option::Some(arg) = args.args.first() {
return arg.to_token_stream();
}
}
}
}
}
// 如果无法提取,返回一般 TokenStream
quote! { () }
}
testbuilder
./testbuilder/Cargo.toml
[package]
name = "testbuilder"
version = "0.1.0"
edition = "2024"
[dependencies]
derive_builder = { path = "../derive_builder" }
./testbuilder/src/main.rs
use derive_builder::Builder;
#[derive(Builder, Debug)]
pub struct Command {
executable: String,
#[builder(each = "arg")]
args: Vec<String>,
#[builder(each = "env")]
env: Vec<String>,
current_dir: Option<String>,
}
fn main() {
// 测试个别 setter 方法
println!("测试 setter 方法:");
let mut builder = Command::builder();
builder.executable("cargo".to_owned());
builder.args(vec!["build".to_owned(), "--release".to_owned()]);
builder.env(vec![]);
// 注意未设置 current_dir,因为它是 Option<String> 类型
let command = builder.build().unwrap();
println!("Command: {:?}", command);
assert_eq!(command.executable, "cargo");
assert_eq!(command.current_dir, None);
// 测试链式调用
println!("\n测试链式调用:");
let command = Command::builder()
.executable("cargo".to_owned())
.args(vec!["build".to_owned(), "--release".to_owned()])
.env(vec![])
.current_dir("..".to_owned())
.build()
.unwrap();
println!("Command: {:?}", command);
assert_eq!(command.executable, "cargo");
assert_eq!(command.current_dir, Some("..".to_owned()));
// 测试重复字段的 each 方法
println!("\n测试重复字段:");
let command = Command::builder()
.executable("cargo".to_owned())
.arg("build".to_owned())
.arg("--release".to_owned())
.build()
.unwrap();
println!("Command: {:?}", command);
assert_eq!(command.executable, "cargo");
assert_eq!(command.args, vec!["build", "--release"]);
}
宏展开
利用 cargo-expand 这个 cli 工具便可以将宏进行展开, 以便于查看和理解, 同时应用于调试
# 安装 cargo-expand
cargo install cargo-expand
使用 cargo-expand
cd .\testbuilder
cargo expand
会查看到 .\testbuilder\src\main.rs 的宏展开后的完整代码,
根据完整代码便可以去分析宏的问题, 进而做出修改, 并且后续再通过 cargo expand
进行展开分析.
如下面所示
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2024::*;
#[macro_use]
extern crate std;
use derive_builder::Builder;
pub struct Command {
executable: String,
#[builder(each = "arg")]
args: Vec<String>,
#[builder(each = "env")]
env: Vec<String>,
current_dir: Option<String>,
}
pub struct CommandBuilder {
executable: std::option::Option<String>,
args: std::option::Option<Vec<String>>,
env: std::option::Option<Vec<String>>,
current_dir: Option<String>,
}
impl Command {
pub fn builder() -> CommandBuilder {
CommandBuilder {
executable: std::option::Option::None,
args: std::option::Option::None,
env: std::option::Option::None,
current_dir: std::option::Option::None,
}
}
}
impl CommandBuilder {
pub fn executable(&mut self, executable: String) -> &mut Self {
self.executable = std::option::Option::Some(executable);
self
}
pub fn args(&mut self, args: Vec<String>) -> &mut Self {
self.args = std::option::Option::Some(args);
self
}
pub fn arg(&mut self, item: String) -> &mut Self {
if self.args.is_none() {
self.args = std::option::Option::Some(std::vec::Vec::new());
}
self.args.as_mut().unwrap().push(item);
self
}
pub fn env(&mut self, env: Vec<String>) -> &mut Self {
self.env = std::option::Option::Some(env);
self
}
pub fn current_dir(&mut self, current_dir: String) -> &mut Self {
self.current_dir = std::option::Option::Some(current_dir);
self
}
pub fn build(&self) -> std::result::Result<Command, std::string::String> {
std::result::Result::Ok(Command {
executable: self.executable.clone().unwrap_or_default(),
args: self.args.clone().unwrap_or_default(),
env: self.env.clone().unwrap_or_default(),
current_dir: self.current_dir.clone(),
})
}
}
#[automatically_derived]
impl ::core::fmt::Debug for Command {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field4_finish(
f,
"Command",
"executable",
&self.executable,
"args",
&self.args,
"env",
&self.env,
"current_dir",
&&self.current_dir,
)
}
}
fn main() {
{
::std::io::_print(format_args!("测试 setter 方法:\n"));
};
let mut builder = Command::builder();
builder.executable("cargo".to_owned());
builder
.args(
<[_]>::into_vec(
::alloc::boxed::box_new(["build".to_owned(), "--release".to_owned()]),
),
);
builder.env(::alloc::vec::Vec::new());
let command = builder.build().unwrap();
{
::std::io::_print(format_args!("Command: {0:?}\n", command));
};
match (&command.executable, &"cargo") {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(
kind,
&*left_val,
&*right_val,
::core::option::Option::None,
);
}
}
};
match (&command.current_dir, &None) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(
kind,
&*left_val,
&*right_val,
::core::option::Option::None,
);
}
}
};
{
::std::io::_print(format_args!("\n测试链式调用:\n"));
};
let command = Command::builder()
.executable("cargo".to_owned())
.args(
<[_]>::into_vec(
::alloc::boxed::box_new(["build".to_owned(), "--release".to_owned()]),
),
)
.env(::alloc::vec::Vec::new())
.current_dir("..".to_owned())
.build()
.unwrap();
{
::std::io::_print(format_args!("Command: {0:?}\n", command));
};
match (&command.executable, &"cargo") {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(
kind,
&*left_val,
&*right_val,
::core::option::Option::None,
);
}
}
};
match (&command.current_dir, &Some("..".to_owned())) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(
kind,
&*left_val,
&*right_val,
::core::option::Option::None,
);
}
}
};
{
::std::io::_print(format_args!("\n测试重复字段:\n"));
};
let command = Command::builder()
.executable("cargo".to_owned())
.arg("build".to_owned())
.arg("--release".to_owned())
.build()
.unwrap();
{
::std::io::_print(format_args!("Command: {0:?}\n", command));
};
match (&command.executable, &"cargo") {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(
kind,
&*left_val,
&*right_val,
::core::option::Option::None,
);
}
}
};
match (
&command.args,
&<[_]>::into_vec(::alloc::boxed::box_new(["build", "--release"])),
) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(
kind,
&*left_val,
&*right_val,
::core::option::Option::None,
);
}
}
};
}
宏代码解析
关键数据结构
-
BuilderFieldOptions - 用于从字段属性中提取配置
skip
: 是否跳过该字段each
: 用于处理重复字段,指定单项添加方法的名称
-
BuilderOptions - 从派生输入中提取信息
- 保存结构体标识符和解析后的字段数据
-
BuilderField - 表示构建器中的一个字段
- 包含字段名称、类型和属性设置
工作流程
- 解析输入 - 解析传入的 TokenStream 为 AST(抽象语法树)
- 提取字段 - 从结构体定义中提取所有字段
- 处理字段 - 为每个字段生成对应的构建器字段和 setter 方法
- 生成代码 - 使用 quote 宏生成最终的构建器代码
字段处理逻辑
- 普通字段 - 在构建器中转换为
Option<T>
类型,提供基本 setter 方法 - 已是 Option 的字段 - 保持 Option 类型,在构建时如果为 None 则保持 None
- Vec 类型字段 - 如果设置了
each
属性,生成额外的单项添加方法
使用方法
基本用法
use my_builder_macro::Builder;
#[derive(Builder, Debug)]
pub struct Person {
name: String,
age: u32,
address: Option<String>,
}
fn main() {
let person = Person::builder()
.name("张三".to_string())
.age(30)
.build()
.unwrap();
println!("{:?}", person); // Person { name: "张三", age: 30, address: None }
}
处理重复字段 (Vec)
使用 each
属性为 Vec 类型字段添加单项添加方法:
#[derive(Builder, Debug)]
pub struct Command {
executable: String,
#[builder(each = "arg")]
args: Vec<String>,
}
fn main() {
// 使用单项添加方法
let cmd = Command::builder()
.executable("cargo".to_string())
.arg("build".to_string())
.arg("--release".to_string())
.build()
.unwrap();
// 或者一次性添加整个集合
let cmd2 = Command::builder()
.executable("cargo".to_string())
.args(vec!["build".to_string(), "--release".to_string()])
.build()
.unwrap();
}
可选字段
对于 Option<T>
类型的字段,如果未设置则默认为 None
:
#[derive(Builder, Debug)]
pub struct Config {
name: String,
timeout: Option<u32>,
}
fn main() {
// timeout 是可选的,不设置也能成功构建
let config = Config::builder()
.name("app".to_string())
.build()
.unwrap();
assert_eq!(config.timeout, None);
}
实现细节
处理必填字段
在构建过程中,对于非 Option 类型的字段,如果未设置则会返回错误:
impl #builder_name {
pub fn build(&self) -> Result<#name, String> {
Ok(#name {
#(#builder_build_fields,)*
})
}
}
其中,对于非 Option 类型的字段,生成类似于:
field_name: self.field_name.clone().ok_or("field_name is not set")?
处理 Vec 类型和 each 属性
对于 Vec 类型且有 each 属性的字段,生成额外的单项添加方法:
pub fn item_name(&mut self, item: ItemType) -> &mut Self {
if self.field_name.is_none() {
self.field_name = Some(Vec::new());
}
self.field_name.as_mut().unwrap().push(item);
self
}
最佳实践
- 明确标记可选字段 - 使用
Option<T>
明确哪些字段是可选的 - 为集合类型添加 each 属性 - 对于 Vec 类型字段,使用
#[builder(each = "xxx")]
提供更灵活的接口 - 使用描述性名称 - 为 each 属性选择清晰、描述性的名称
- 处理所有错误 - 在使用 builder 时,适当处理 build() 可能返回的错误
高级定制
这个实现可以进一步扩展支持以下功能:
- 默认值 - 添加
default
属性,为字段提供默认值 - 验证函数 - 添加属性来指定验证函数,在构建时验证字段值
- 自定义错误类型 - 使用自定义错误类型而不是简单的字符串
- 私有字段访问 - 处理结构体中的私有字段
本文是原创文章,采用 CC BY-NC-ND 4.0 协议,完整转载请注明来自 Unic
评论
匿名评论
隐私政策
你无需删除空行,直接评论以获取最佳展示效果