rust聲明式宏的實現(xiàn)
在 rust 中,我們一開始就在使用宏,例如 println!, vec!, assert_eq! 等。看起來宏和函數(shù)在使用時只是多了一個 !。實際上這些宏都是聲明式宏(也叫示例宏或macro_rules!),rust 還支持過程宏,過程宏為我們提供了強大的元編程工具。
聲明式宏
聲明式宏類似于 match 匹配。它可以將表達(dá)式的結(jié)果與多個模式進(jìn)行匹配。一旦匹配成功,那么該模式相關(guān)聯(lián)的代碼將被展開。和 match 不同的是,宏里的值是一段 rust 源代碼。所有這些都發(fā)生在編譯期,并沒有運行期的性能損耗。下面是一個例子:
// 聲明一個add宏 macro_rules! add { ($a: expr, $b: expr) => { $a + $b }; } fn main() { let a = 10; let b = 22; let _res = add!(a, b); let _res = add!(a+1, b); let _res = add!(a*2, b+3); }
我們需要一個類似于 GCC -E 的方式來查看一下預(yù)處理階段之后的代碼。cargo-expand 正好提供了相應(yīng)的功能。使用 cargo 安裝 cargo-expand 即可。
cargo install cargo-expand
安裝 cargo-expand 之后,可以使用 cargo expand 命令來查看聲明式宏是如何被展開的。上面的代碼在執(zhí)行cargo expand之后輸出如下所示:
#![feature(prelude_import)] #[prelude_import] use std::prelude::rust_2021::*; #[macro_use] extern crate std; fn main() { let a = 10; let b = 22; let _res = a + b; let _res = a + 1 + b; let _res = a * 2 + (b + 3); }
可以看到,每一個 _res 的右邊都被展開了,并且如果傳入的參數(shù)是一個表達(dá)式,則會將整個表達(dá)式作為一個整體傳遞給宏。這就是某些地方提到的“Hygienic Macros”(有些地方也翻譯為衛(wèi)生宏,翻譯的很抽象)。最后一行代碼中傳入的b+3被當(dāng)做了一個整體。如果是在C/C++中,不會自動將表達(dá)式作為整體,而是直接進(jìn)行字符串替換。而 Rust 編譯器會自動處理變量名和作用域,確保宏展開后的代碼不會引入未預(yù)料的變量沖突。下面是一個C/C++中使用宏的例子。
#include<stdio.h> #define ADD(a, b) a + b; int main() { int a = 10; int b = 22; int _res = ADD(a, b) _res = ADD(a+1, b) _res = ADD(a*2, b+3) }
同樣,我們使用 gcc -E main.c 來獲取預(yù)處理之后的代碼。由于展開之后的代碼非常得多,我們只放上 main 函數(shù)中展開的部分。
int main() { int a = 10; int b = 22; int _res = a + b; _res = a+1 + b; _res = a*2 + b+3; }
可以看到,調(diào)用的代碼展開之后,并沒有將 b+3 作為一個整體來處理,而是簡單的進(jìn)行替換。因此,我們在 C/C++ 中編寫宏要特別注意,宏參數(shù)在使用的時候必須加上括號?,F(xiàn)在我們來修復(fù)上面 C/C++ 代碼中的宏。
#include<stdio.h> #define ADD(a, b) (a) + (b); int main() { int a = 10; int b = 22; int _res = ADD(a, b) _res = ADD(a+1, b) _res = ADD(a*2, b+3) }
這樣,我們在使用宏的時候,就避免了意外結(jié)果的發(fā)生。這樣展開之后的代碼如下所示:
int main() { int a = 10; int b = 22; int _res = (a) + (b); _res = (a+1) + (b); _res = (a*2) + (b+3); }
我們接著來定義我們自己的 my_vec! 宏, 來對聲明式宏的相關(guān)語法做一個解釋。
macro_rules! my_vec { // 匹配 my_vec![] () => { std::vec::Vec::new() }; // 匹配 my_vec![1,2,3] ($($el:expr), *) => { // 這段代碼需要用{}包裹起來,因為宏需要展開,這樣能保證作用域正常,不影響外部。這也是rust的宏是 Hygienic Macros 的體現(xiàn)。 // 而 C/C++ 的宏不強制要求,但是如果遇到代碼片段,在 C/C++ 中也應(yīng)該使用{}包裹起來。 { let mut v = std::vec::Vec::new(); $(v.push($el);)* v } }; // 匹配 my_vec![1; 3] ($el:expr; $n:expr) => { std::vec::from_elem($el, $n) }; }
由于宏要在調(diào)用的地方展開,我們無法預(yù)測調(diào)用者的環(huán)境是否已經(jīng)做了相關(guān)的 use,所以我們使用的代碼最好帶著完整的命名空間。
在聲明宏中,條件捕獲的參數(shù)使用
$
開頭的標(biāo)識符來聲明。每個參數(shù)都需要提供類型,這里expr
代表表達(dá)式,所以$el:expr
是說把匹配到的表達(dá)式命名為$el
。$(...),*
告訴編譯器可以匹配任意多個以逗號分隔的表達(dá)式,然后捕獲到的每一個表達(dá)式可以用$el
來訪問。由于匹配的時候匹配到一個$(...)*
(我們可以不管分隔符),在執(zhí)行的代碼塊中,我們也要相應(yīng)地使用$(...)*
展開。所以這句$(v.push($el);)*
相當(dāng)于匹配出多少個$el
就展開多少句 push 語句。
反復(fù)捕獲反復(fù)捕獲的一般形式是$ ( ... ) sep rep
,$ 是字面上的美元符號標(biāo)記 ( ... ) 是被反復(fù)匹配的模式,由小括號包圍。 sep 是可選的分隔標(biāo)記。它不能是括號或者反復(fù)操作符 rep。常用例子有 , 和 ; 。 rep 是必須的重復(fù)操作符。當(dāng)前可以是: 1. ?:表示最多一次重復(fù),所以此時不能前跟分隔標(biāo)記。 2. *:表示零次或多次重復(fù)。 3. +:表示一次或多次重復(fù)。
如果傳入用冒號分隔的兩個表達(dá)式,那么會用 from_element 構(gòu)建 Vec。
我們來使用一下自定義的 my_vec! 宏
let mut v = my_vec!(); v.push(1); println!("{:?}", v); let v = my_vec![1, 2, 3, 4, 5]; println!("{:?}", v); let v = my_vec!{1; 3}; println!("{:?}", v);
我們在使用宏的時候,可以使用(), [], {},都是可以的。但是一般都是按照約定成俗的方式來使用。例如:vec![1,2,3]
,而不是使用 vec!{1,2,3}
。
這段宏調(diào)用,展開以后,如下所示:
let mut v = std::vec::Vec::new(); v.push(1); { ::std::io::_print(format_args!("{0:?}\n", v)); }; let v = { let mut v = std::vec::Vec::new(); v.push(1); v.push(2); v.push(3); v.push(4); v.push(5); v }; { ::std::io::_print(format_args!("{0:?}\n", v)); }; let v = std::vec::from_elem(1, 3); { ::std::io::_print(format_args!("{0:?}\n", v)); };
可以看到,let v = my_vec![1, 2, 3, 4, 5];
被展開為
let v = { let mut v = std::vec::Vec::new(); v.push(1); v.push(2); v.push(3); v.push(4); v.push(5); v };
它帶上了我們在宏定義中的{},另外我們注意到println! 宏也被展開了, 但是并沒有完全展開,其中還包含了一個format_args! 宏,我們來看一下,是否和println宏的定義一樣。
// println宏的定義 macro_rules! println { () => { $crate::print!("\n") }; ($($arg:tt)*) => {{ $crate::io::_print($crate::format_args_nl!($($arg)*)); }}; }
可以看到,println帶有參數(shù)將會使用 format_args_nl! 宏,但是expand確是 format_args 宏。大概可能是因為文檔中說format_args_nl宏是nightly模式下的吧!并沒有完全展開是因為該宏是內(nèi)置宏(rustc_builtin_macro)。
在使用聲明宏時,我們需要為參數(shù)明確類型,剛才的例子都是使用的expr,其實還可以使用下面這些:
- item,比如一個函數(shù)、結(jié)構(gòu)體、模塊等。
- block,代碼塊。比如一系列由花括號包裹的表達(dá)式和語句。
- stmt,語句。比如一個賦值語句。
- pat,模式。
- expr,表達(dá)式。剛才的例子使用過了。
- ty,類型。比如 Vec。
- ident,標(biāo)識符。比如一個變量名。
- path,路徑。比如:foo、::std::mem::replace、transmute::<_, int>。 meta,元數(shù)據(jù)。一般是在
#[...]`` 和
#![…]`` 屬性內(nèi)部的數(shù)據(jù)。 - tt,單個的 token 樹。
- vis,可能為空的一個 Visibility 修飾符。比如 pub、pub(crate)
聲明式宏還算比較簡單。它可以幫助我們解決一些問題。
- 代碼重復(fù):聲明式宏可以幫助消除代碼中的冗余,通過將重復(fù)的代碼邏輯抽象成宏,從而減少代碼量并提高代碼的可讀性和維護性。
- 代碼模板化:宏可以用于定義代碼模板,允許在編譯時根據(jù)不同的參數(shù)生成特定的代碼片段,從而實現(xiàn)代碼的泛化和重用。
- 實現(xiàn)函數(shù)重載,宏可以匹配多種模式的參數(shù)來實現(xiàn)函數(shù)重載。
宏的缺點
宏目前的編寫無法得到IDE很好的支持,另外一點就是如無必要,就不要編寫宏。如果要編寫,那么盡量編寫聲明式宏,而不是過程宏。
- 宏編寫復(fù)雜:過程宏的編寫可能相對復(fù)雜,特別是對于復(fù)雜的語法分析和代碼生成任務(wù),編寫和調(diào)試過程宏可能需要更多的時間和精力。
- 可讀性下降:宏可能會導(dǎo)致代碼的可讀性下降,特別是在宏的展開代碼復(fù)雜或嵌套層級較多時,代碼可讀性可能變差。
- 不利于錯誤檢查:宏展開發(fā)生在編譯期間,因此錯誤信息可能不夠明確和直觀,難以定位宏展開后的具體錯誤位置。
- 難以調(diào)試:宏展開過程對于開發(fā)者不是透明的,因此在調(diào)試過程中可能會遇到難以解決的問題。
參考資料
- https://blog.logrocket.com/macros-in-rust-a-tutorial-with-examples/#:~:text=Declarative%20macros%20enable%20you%20to,Rust%20code%20it%20is%20given.
- rust編程第一課-陳天
- The Little Book of Rust Macros
到此這篇關(guān)于rust聲明式宏的實現(xiàn)的文章就介紹到這了,更多相關(guān)rust聲明式宏內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Rust調(diào)用Windows API 如何獲取正在運行的全部進(jìn)程信息
本文介紹了如何使用Rust調(diào)用WindowsAPI獲取正在運行的全部進(jìn)程信息,通過引入winapi依賴并添加相應(yīng)的features,可以實現(xiàn)對不同API集的調(diào)用,本文通過實例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-11-11rust標(biāo)準(zhǔn)庫std::env環(huán)境相關(guān)的常量
在本章節(jié)中, 我們探討了Rust處理命令行參數(shù)的常見的兩種方式和處理環(huán)境變量的兩種常見方式, 拋開Rust的語法, 實際上在命令行參數(shù)的處理方式上, 與其它語言大同小異, 可能影響我們習(xí)慣的也就只剩下語法,本文介紹rust標(biāo)準(zhǔn)庫std::env的相關(guān)知識,感興趣的朋友一起看看吧2024-03-03關(guān)于Rust命令行參數(shù)解析以minigrep為例
本文介紹了如何使用Rust的std::env::args函數(shù)來解析命令行參數(shù),并展示了如何將這些參數(shù)存儲在變量中,隨后,提到了處理文件和搜索邏輯的步驟,包括讀取文件內(nèi)容、搜索匹配項和輸出搜索結(jié)果,最后,總結(jié)了Rust標(biāo)準(zhǔn)庫在命令行參數(shù)處理中的便捷性和社區(qū)資源的支持2025-02-02