前言
rust 学习曲线非常陡峭,但是基本语法也还算挺好理解,自动内存管理有点类似智能指针,基本看一下语法入门就可以大概理解,但是唯独宏很难理解,语法非常晦涩。但是功能非常强大。声明宏类似于c语言的宏处理,但是功能更强大。过程宏则类似于Android的注解编程,自定义AbstractProcessor
,但是实现更优雅。
下面记录一下宏处理的一些特点
正文
目前主流的文章都是翻译自官方文档,或者取部分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
,举个例子,就是用宏为一个结构体实现构建者模式。
#[derive(Builder)]
struct Command {// ...
}
最麻烦的是如何实现Builder
#[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_fieldspub fn build(self) -> Result<#input_ident, String> {#builder_letsOk(#input_ident{ #builder_fields_values })}}).into()} else {// 不支持非struct类型quote!().into()}
}fn map_fields<F>(fields: &Fields, mapper:&mut F) -> TokenStream2
whereF: 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方法如下:
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需要依赖一些库,这比较复杂,就不在详细介绍。要结合自己代码需求,慢慢理解。
分析工具
cargo.exe install cargo-expandcargo.exe expand
后记
rust实在是复杂,这里解释一些语法规则,以后遇到问题再补充。