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