一文弄懂rust聲明宏
Rust支持兩種宏,一種是聲明宏,一種是過程宏,前者相較于后者還是比較簡單的。本文主要是講解Rust元編程里的聲明宏,通過聲明宏可以減少一些樣板代碼,它是一個用代碼生成代碼的技術。
聲明宏的主要原理是通過匹配傳入的代碼然后替換成指定的代碼,因為替換是發(fā)生在編譯器,所以rust的宏編程沒有任何運行時的開銷,可以放心的用,不用擔心性能 :)。
快速入門
聲明宏不像過程宏那樣需要在單獨的包(package/crate)中定義,只需要使用macro_rules!就可以簡單的定義一個聲明宏,一個簡單的示例如下。
// https://youerning.top/post/rust-declarative-macros-tutorial/
macro_rules! add {
($a:expr, $b:expr) => {
$a + $b
};
}
fn main() {
let sum = add!(1,2);
println!("sum: {sum}");
}輸出如下:
sum: 3
上面這個結(jié)果應該不會讓人意外,你會發(fā)現(xiàn)聲明宏定義的那一段代碼和普通的match代碼非常相似,不同的在于變量前面多了個前綴$, 而且需要通過冒號:注明變量的類型,這里的變量類型是expr,這是表達式的意思。
聲明宏語法
一個聲明宏大致可以分為三個部分
- 聲明宏的名稱定義,比如例子中的add
- 模式匹配部分, 比如例子中的($a:expr, $b:expr)
- 聲明宏返回的部分, 也就是花括號被包裹的部分, 比如例子中的$a + $b
本文的開頭說過,過程宏的原理就是通過匹配傳入的代碼然后替換成指定的代碼, 所以上面的例子在編譯(展開)之后應該會變成下面的代碼。
fn main() {
let sum = 1 + 2;
println!("sum: {sum}");
}如果我們傳遞三個參數(shù)呢? 比如add!(1,2,3),那么它會在編譯的時候報以下錯誤。
error: no rules expected the token `,`
--> src\main.rs:8:23
|
1 | macro_rules! add {
| ---------------- when calling this macro
...
8 | let sum = add!(1,2,3);
| ^ no rules expected this token in macro call
|
note: while trying to match meta-variable `$b:expr`
--> src\main.rs:2:15
|
2 | ($a:expr, $b:expr)=>{
| ^^^^^^^error: could not compile `declarative-macros` (bin "declarative-macros") due to previous error
其實這很好理解,我們的模式只能匹配兩個變量$a和$b, 但是add!(1,2,3)卻傳入了三個變量,所以匹配不了,那么就會報錯,因為這是不合法的語法。
那么,怎么匹配三個變量,或者是一個變量呢? 有兩個辦法,一是一一對應,二是使用重復的匹配方法。為了簡單起見,我們先使用比較笨的方法,代碼如下。
macro_rules! add {
// 聲明宏的第一條匹配規(guī)則
($a: expr) => {
$a
};
// 聲明宏的第二條匹配規(guī)則
($a:expr, $b:expr)=>{
$a + $b
};
// 聲明宏的第三條匹配規(guī)則
($a:expr, $b:expr, $c: expr)=>{
$a + $b
};
}
fn main() {
let sum = add!(1);
println!("sum1: {sum}");
let sum = add!(1,2);
println!("sum2: {sum}");
let sum = add!(1,2,3);
println!("sum3: {sum}");
}上面的代碼和快速入門的例子沒有太大的區(qū)別,主要的區(qū)別是之前的例子只有一個匹配規(guī)則,而新的例子有三條匹配規(guī)則,當rust編譯代碼的時候,會將調(diào)用聲明宏的輸入?yún)?shù)從上至下依次匹配每條規(guī)則,當匹配到就會停止匹配,然后返回對應的代碼,這和rust的match模式匹配沒有太大的區(qū)別,唯一的區(qū)別可能是, 聲明宏使用;分隔不同的匹配模式,而match的不同匹配模式使用,分隔。
上面的代碼輸出如下:
sum1: 1
sum2: 3
sum3: 3
這樣的結(jié)果并不讓人意外,唯一讓人沮喪的是,每種情況都寫一個對應的表達式的話,得累死去。
元變量
現(xiàn)在讓我們繼續(xù)看看rust的聲明宏支持哪些類型。
- item: 條目,比如函數(shù)、結(jié)構(gòu)體、模組等。
- block: 區(qū)塊(即由花括號包起的一些語句加上/或是一項表達式)。
- stmt: 語句
- pat: 模式
- expr: 表達式
- ty: 類型
- ident: 標識符
- path: 路徑 (例如 foo, ::std::mem::replace, transmute::<_, int>, …)
- meta: 元條目,即被包含在 #[...]及#![...]屬性內(nèi)的東西。
- tt: 標記樹
大多數(shù)情況,一般只會使用expr和tt, 使用expr是因為rust中幾乎可以被稱為基于表達式的編程語言,因為它的表達式概念非常大,即使是if和while這樣的語句也可以作為一個表達式返回值,而tt是一個萬金油,它可以簡單的被認為是其他類型都不匹配的情況下的兜底類型。
下面看一個tt類型的例子。
macro_rules! add {
($a: tt) => {
{
println!("{}", stringify!($a));
1
}
};
}
fn main() {
let sum = add!(1);
println!("sum: {sum}");
let sum = add!(,);
println!("sum: {sum}");
let sum = add!({});
println!("sum: {sum}");
let sum = add!(youerning);
println!("sum: {sum}");
}代碼輸出如下:
1
sum: 1
,
sum: 1
{}
sum: 1
youerning
sum: 1
代碼展開后長這樣:
值得注意的是: 下面的代碼是手動的展開,與真實的編譯代碼還是有點區(qū)別的!!!
fn main() {
let sum = {
println!("{}", "1")
1
};
println!("sum: {sum}");
let sum = {
println!("{}", ",")
1
};
println!("sum: {sum}");
let sum = {
println!("{}", "{}")
1
};
println!("sum: {sum}");
}總的來說, tt這個類型可以接受合法或者不合法的各種標識符。
stringify!是啥? 說實話我也不太懂,我的理解是,你可以將任何東西扔給它,它會返回一個字符串字面量給你。
宏展開(expand)
如果我真的能夠手動展開自己的代碼,那就肯定會了,也就不用開文章學習了不是,所以如果吃不準宏展開之后的結(jié)果或者故障排查的時候可以使用cargo expand命令查看展開后的代碼。
可以通過以下命令安裝。
cargo install cargo-expand
安裝之后在項目的根目錄執(zhí)行cargo expand即可,上面的例子展開之后如下。
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
fn main() {
let sum = {
{
::std::io::_print(format_args!("{0}\n", "1"));
};
1
};
{
::std::io::_print(format_args!("sum: {0}\n", sum));
};
let sum = {
{
::std::io::_print(format_args!("{0}\n", ","));
};
1
};
{
::std::io::_print(format_args!("sum: {0}\n", sum));
};
let sum = {
{
::std::io::_print(format_args!("{0}\n", "{}"));
};
1
};
{
::std::io::_print(format_args!("sum: {0}\n", sum));
};
let sum = {
{
::std::io::_print(format_args!("{0}\n", "youerning"));
};
1
};
{
::std::io::_print(format_args!("sum: {0}\n", sum));
};
}如果看不太懂可以結(jié)合我手動展開的代碼一起看。
標記樹撕咬機(TT muncher)
通過標記樹撕咬機(TT muncher)我們可以實現(xiàn)遞歸的聲明宏,不過在此之前讓我們先解決不定參數(shù)的問題,之前解決的方案是根據(jù)要傳的參數(shù)編寫聲明宏的匹配代碼,這樣實在是太不優(yōu)雅了,讓我們看看怎么一次性搞定。
macro_rules! add {
($($a: expr),*) => {
0$(+$a)*
};
}
fn main() {
let sum = add!();
println!("sum1: {sum}");
let sum = add!(1);
println!("sum1: {sum}");
let sum = add!(1,2);
println!("sum2: {sum}");
let sum = add!(1,2,3);
println!("sum3: {sum}");
}輸出如下:
sum1: 0
sum1: 1
sum2: 3
sum3: 6
重復
聲明宏里面有一些難點,其中一個就是重復的匹配模式, 也就是這個例子中的$($a: expr),*, 為啥要這樣寫? 因為這是rust的語法, 就像定義一個新變量必須使用let表達式一樣,這個不需要太糾結(jié)。
下面來看看這種模式的語法定義,重復的一般形式是$ ( ... ) sep rep
- $ 是字面標記。
- ( ... ) 代表了將要被重復匹配的模式,由小括號包圍。
- sep是一個可選的分隔標記。常用例子包括,和;。
- rep是重復控制標記。當前有兩種選擇,分別是* (代表接受0或多次重復)以及+ (代表1或多次重復)。目前沒有辦法指定“0或1”或者任何其它更加具體的重復計數(shù)或區(qū)間。
大家可以將($($a: expr),*)改成($($a: expr);*),然后就會發(fā)現(xiàn)編譯不過了,因為分隔符需要是;了
也就是說, $($a: expr),*匹配到了(), (1), (1,2),(1,2,3),為啥能匹配到()?, 因為*能匹配0個或多個,所以零參數(shù)的()也能匹配上,如果你將這個例子中的*換成+,就會發(fā)現(xiàn)add!()會報錯,因為+要求至少一個參數(shù)。
下面以參數(shù)(1,2,3)的例子再深入一下宏展開時的操作,當傳入(1,2,3)時,因為跟$($a: expr),*能夠匹配上, 所以(1,2,3)里的冒號,被$($a: expr),*的冒號,給匹配上,而$a代表1 2 3中的每個元素, 那么怎么在返回的代碼中標識重復的參數(shù)呢?rust的語法是, 我們需要使用$()*將$a包裹起來,外面的包裝代碼對應參數(shù)匹配時的重復次數(shù), 你可以簡單的將$()*認為是必要的語法。
下面看一個簡單的例子
macro_rules! print {
($($a: expr),*) => {
println!("{} {}", $($a),*)
};
}
fn main() {
print!(1,2);
}$($a),*會原封不動的將參數(shù)放在它對應的位置,因為println!指定了兩個位置參數(shù),所以使用自定義的print只能傳遞兩個參數(shù)。
最后看看上面那個add!宏的例子, add!(1,2,3)展開之后應該變成下面這樣。
0+1+2+3
之所以這樣,是因為我們在返回的代碼模式中$($a)*在$a前面加了一個+, 而這個加號+因為被$()*包裹,所以會跟著$a重復一樣的次數(shù),也就變成了+1+2+3。
為啥前面要加個0?因為不加0的話, 就不是合法的表達式了。
遞歸示例1
雖然add!這個宏可以使用一個模式匹配就能完成,但是我們可以使用更加復雜的方式實現(xiàn),也就是標記樹撕咬機(TT muncher)。
macro_rules! add {
($a: expr) => {
$a
};
($a: expr, $b: expr) => {
$a + $b
};
($a: expr, $($other: tt)*) => {
$a + add!($($other)*)
};
}
fn main() {
let sum = add!(1,2,3,4,5);
println!("sum: {sum}");
}使用**標記樹撕咬機(TT muncher)**的代碼和之前的代碼結(jié)果沒有什么區(qū)別,但是展開的過程中會有些不同,因為后者使用了遞歸,它的遞歸調(diào)用類似于add!(1, add!(2, add!(3, add!(3, add!(3, add!(5))))));
這段代碼的前兩個匹配模式不用過多介紹,關鍵在于最后一個($a: expr, $($other: tt)*), $a 和 ,會吃掉一個參數(shù)和一個逗號,, 而$($other: tt)*會匹配到后面所有的參數(shù)2,3,4,5。
注意這些參數(shù)包含逗號,, 還有就是我們在使用$($other: tt)*這種重復模式的時候沒有指定分隔符, 所以tt既匹配了參數(shù)2 3 4 5也匹配了分割這些數(shù)字的逗號,, 所以在展開的代碼$a + add!($($other)*)會變成1 + add!(2,3,4,5), 然后就是不斷的遞歸了,直到遇到第一個匹配模式。
遞歸示例2
你可能在上一個例子不能感受到**標記樹撕咬機(TT muncher)**的威力,所以我們繼續(xù)看下一個例子。
我們可以通過**標記樹撕咬機(TT muncher)**的遞歸調(diào)用來生成對嵌套對象的遞歸調(diào)用,這樣就不需要不斷的判斷Option的值是Some還是None了。
use serde_json::{json, Value};
macro_rules! serde_get {
($value: ident, $first: expr) => {
{
match ($value).get($first) {
Some(val) => Some(val),
None => {
None
}
}
}
};
($value: ident, $first: expr, $($others:expr),+) => {
{
match ($value).get($first) {
Some(val) => {
serde_get!(val, $($others),+)
},
None => {
None
}
}
}
};
($value: ident, $first: expr, $($others:tt)* ) => {
{
match ($ident).get($first) {
Some(val) => {
serde_get!(val, $($others)+),
}
None => None
}
}
};
}
fn main() {
let object = json!({
"key11": {"key12": "key13"},
"key21": {"key22": {"key23": "key24"}}
});
if let Some(val) = serde_get!(object, "xx") {
println!(r#"object["a"]["b"]["c"]={val:?}"#);
} else {
println!(r#"object["a"]["b"]["c"]不存在"#);
}
if let Some(val) = serde_get!(object, "key1", "key12") {
println!(r#"object["key11"]["key12"] = {val:}"#);
}
if let Some(val) = serde_get!(object, "key21", "key22", "key23") {
println!(r#"object["key21"]["key21"]["key23"] = {val:}"#);
}
}這個例子寫完,我才發(fā)現(xiàn)serde_json可以直接使用["key21"]["key21"]["key23"]這樣的語法直接判斷!!!, 不過serde_json的返回結(jié)果都是null, 如果鍵值對不存在的話。
總結(jié)
我感覺rust的宏編程還是很有意思的,不過這東西的確得真正有需求的時候才會真的理解,我之前也不是太懂,看了視頻和文章也不是太懂,只是知道它能干啥,但是沒有一個真正要解決的問題,所以一直不能很好的掌握,直到在使用serde_json時遇到嵌套的數(shù)據(jù)結(jié)構(gòu)需要寫重復的判斷代碼時,我才在應用的時候掌握了聲明宏(雖然最后發(fā)現(xiàn)它的實用價值可能不是那么大),至于過程宏,可能等我遇到需要過程宏的時候才會很好的掌握吧,到時候在寫對應的文章吧。
參考鏈接
https://earthly.dev/blog/rust-macros/
https://doc.rust-lang.org/reference/macros-by-example.html#metavariables
https://www.bookstack.cn/read/DaseinPhaos-tlborm-chinese/mbe-macro-rules.md
https://veykril.github.io/tlborm/
https://github.com/dtolnay/cargo-expandhttps://youerning.top/post/rust/rust-declarative-macros-tutorial/
到此這篇關于一文弄懂rust聲明宏的文章就介紹到這了,更多相關rust聲明宏內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
如何使用VSCode配置Rust開發(fā)環(huán)境(Rust新手教程)
這篇文章主要介紹了如何使用VSCode配置Rust開發(fā)環(huán)境(Rust新手教程),本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-07-07
C和Java沒那么香了,Serverless時代Rust即將稱王?
Serverless Computing,即”無服務器計算”,其實這一概念在剛剛提出的時候并沒有獲得太多的關注,直到2014年AWS Lambda這一里程碑式的產(chǎn)品出現(xiàn)。Serverless算是正式走進了云計算的舞臺2021-06-06

