前言
rust 学习曲线非常陡峭,但是基本语法也还算挺好理解,自动内存管理有点类似智能指针,基本看一下语法入门就可以大概理解,但是唯独宏很难理解,语法非常晦涩。但是功能非常强大。声明宏类似于c语言的宏处理,但是功能更强大。过程宏则类似于Android的注解编程,自定义AbstractProcessor
,但是实现更优雅。
下面记录一下宏处理的一些特点
正文
目前主流的文章都是翻译自官方文档,或者取部分Rust语言圣经
,关键的部分特性就确实,只有rust宏详解中非常详细的介绍,这里简要记录一下特点
声明宏
声明宏主要是替代,主要是通过简单的模式匹配,然后进行操作,这貌似非常容易处理面向对象的工厂模式,或者解决方法多参数操作,比如
rust
macro_rules! vec {
() => (
$crate::__rust_force_expr!($crate::vec::Vec::new())
);
($elem:expr; $n:expr) => (
$crate::__rust_force_expr!($crate::vec::from_elem($elem, $n))
);
($($x:expr),+ $(,)?) => (
$crate::__rust_force_expr!(<[_]>::into_vec(
// This rustc_box is not required, but it produces a dramatic improvement in compile
// time when constructing arrays with many elements.
#[rustc_box]
$crate::boxed::Box::new([$($x),+])
))
);
}
这就是根据不同的匹配模式,=>的前半部分,替换成后半部。比如无参数的vec。因为这是系统接口,这里不在详细介绍操作,只介绍匹配,
- ()是无参构造函数。
- ($elem:expr; $n:expr)这是匹配模式类似
vec![1;5]
,这是创造一个size是5的,值是1的vec。 - 这个是匹配vec![1, 2, 3],这是构造一个内容是1、2、3的vec
第二个匹配模式中,$elem
是为匹配的内容命名,方便后面使用,expr
(一个表达式 (expression))指明匹配的元素,;就不用解释,就是字面值。$n:expr
同样道理。
第二个匹配则稍微复杂一些,这里则用的是循环模式。循环是通过$(....)
来指明的,括号内为循环内容,为了方便阅读,则需要有分隔符和循环次数,这里是通过,
定义分隔符,+
定义循环至少一次。$(,)?又是一个循环,循环内容则是,
。而循环一次则是最多一次。
所有的语句如下:
block:一个块(比如一块语句或者由大括号包围的一个表达式)
expr:一个表达式 (expression)
ident:一个标识符 (identifier),包括关键字 (keywords)
item:一个条目(比如函数、结构体、模块、impl 块)
lifetime:一个生命周期注解(比如 'foo、'static)
literal:一个字面值(比如 "Hello World!"、3.14、'🦀')
meta:一个元信息(比如 #[...] 和 #![...] 属性内部的东西)
pat:一个模式 (pattern)
path:一条路径(比如 foo、::std::mem::replace、transmute::<_, int>)
stmt:一条语句 (statement)
tt:单棵标记树
ty:一个类型
vis:一个可能为空的可视标识符(比如 pub、pub(in crate))
循环则如下:
反复捕获的一般形式为 $ ( ... ) sep rep。
$ 是字面上的美元符号标记
( ... ) 是被反复匹配的模式,由小括号包围。
sep 是可选的分隔标记。它不能是括号或者反复操作符 rep。常用例子有 , 和 ; 。
rep 是必须的重复操作符。当前可以是:
- ?:表示最多一次重复,所以此时不能前跟分隔标记。
- *:表示零次或多次重复。
- +:表示一次或多次重复。
过程宏
分为三类
- 派生宏(Derive macro):用于结构体(struct)、枚举(enum)、联合(union)类型,可为其实现函数或特征(Trait)。
- 属性宏(Attribute macro):用在结构体、字段、函数等地方,为其指定属性等功能。如标准库中的#[inline]、#[derive(...)]等都是属性宏。
- 函数式宏(Function-like macro):用法与普通的规则宏类似,但功能更加强大,可实现任意语法树层面的转换功能。
声明宏需要解析传入的参数,进行匹配,而过程宏则需要自己解析传入的内容,然后进行补充,生成代码。这里需要解析TokenStream
,举个例子,就是用宏为一个结构体实现构建者模式。
rust
#[derive(Builder)]
struct Command {
// ...
}
最麻烦的是如何实现Builder
rust
#[derive(Builder)]
struct Command {
input_paht: String,
// ...
}
pub fn derive_builder(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput); // 解析input为 DeriveInput类型
let input_ident = input.ident; // 获取原始类名
let ident_builder = format_ident!("{}Builder", input_ident.to_string()); // 拼接builder类名
if let Data::Struct(r) = input.data { // 处理结构体
let fields = r.fields;
// 结构体属性声明
let builder_fields = map_fields(&fields, &mut |(ident, ty)| {
quote!(
#ident: Option<#ty>,
)
});
// 为builder增加set函数
let builder_set_fields = map_fields(&fields, &mut |(ident, ty)| {
quote!(
pub fn #ident(mut self, value: #ty) -> Self {
self.#ident = Some(value);
self
}
)
});
// 获取builder的属性值
let builder_lets = map_fields(&fields, &mut |(ident, _)| {
quote!(
let #ident = self.#ident.ok_or(format!(
"field {:?} not set yet", stringify!(#ident),
))?;
)
});
// 初始化时的默认值
let builder_fields_values = map_fields(&fields, &mut |(ident, _)| {
quote!(
#ident,
)
});
quote!(
impl #input_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<#input_ident, String> {
#builder_lets
Ok(#input_ident{ #builder_fields_values })
}
}
).into()
} else {
// 不支持非struct类型
quote!().into()
}
}
fn map_fields<F>(fields: &Fields, mapper:&mut F) -> TokenStream2
where
F: FnMut((&Option<proc_macro2::Ident> , &Type)) -> TokenStream2,
{
let fs = fields.iter().map(|field| mapper((&field.ident ,&field.ty)) );
let stream2 = TokenStream2::from_iter(fs);
stream2
}
这里为Command
实现了builder方法如下:
rust
impl Command{
pub fn builder() -> CommandBuilder{
CommandBuilder::default()
}
}
pub struct CommandBuilder{
input_path: String,
}
impl CommandBuilder{
pub fn (mut self, value: String) -> Self {
self.input_path = Some(value);
self
}
pub fn build(self) -> Result<Command, String> {
let input_path= self.input_path.ok_or(format!(
"field {:?} not set yet", stringify!(input_path),
))?;
Ok(Command{ input_path })
}
}
属性宏则可以传入参数,让控制更自由一些,这里就不在详细介绍
函数式宏则相对比较简单,类似声明宏,但是可以不去匹配规则,更自由,功能更强大。
解析TokenStream需要依赖一些库,这比较复杂,就不在详细介绍。要结合自己代码需求,慢慢理解。
分析工具
bash
cargo.exe install cargo-expand
cargo.exe expand
后记
rust实在是复杂,这里解释一些语法规则,以后遇到问题再补充。