前言
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實在是復雜,這里解釋一些語法規則,以后遇到問題再補充。