夜雨聆风学习资料网

ARTICLE · 1348188

Rust 里的宏,其实就是“代码模板”

Rust 里的宏,其实就是“代码模板”

上一篇我们讲了怎么发布 crate。

这一篇讲 声明宏 macro_rules!。

很多人一听“宏”就头大。其实用一句话就能理解:

宏就是代码模板。

你写一个模板,告诉编译器“遇到这个写法,就按模板替换”。编译器在编译的时候帮你把模板填好,生成真正的代码。

就这么简单。

一、宏和函数,差在哪?

先看函数:

rust

fn add(a: i32, b: i32) -> i32 {    a + b}

函数的参数是值。调用 add(1, 2),1 和 2 已经算好了,传进去。

宏不一样。宏接收的是代码本身。

rust

println!("hello");

println! 拿到的不是“hello 这个值”,而是 "hello" 这段代码。然后它把这段代码展开成一大段打印逻辑。

打个比方:

* 函数像自动售货机:你投币,它出货。币和货都是“东西”。

* 宏像施工图纸:你给图纸,它照着盖房子。图纸不是房子,是“怎么盖”的描述。

所以:

函数处理值,宏处理代码。

二、最简单的宏

rust

macro_rules! say_hello {    () => {        println!("hello");    };}fn main() {    say_hello!();}

一行行看:

* macro_rules!:我要定义一个宏。

* say_hello:宏的名字。

* () => { ... }:规则。左边是“长什么样”,右边是“替换成什么”。

* say_hello!():调用宏。名字后面必须带 !。

编译的时候,say_hello!() 会被原地替换成 println!("hello")。

运行输出:

text

hello

就这么回事。宏就是“看到这个,换成那个”。

三、带参数的宏

rust

macro_rules! square {    ($x:expr) => {        $x * $x    };}fn main() {    let n = square!(5);    println!("{}", n); // 25}

拆开看:

* $x:挖一个坑,名字叫 x。

* :expr:这个坑里只能填表达式。

* $x * $x:把坑里的东西填进来,算两次。

调用 square!(5),编译器看到 5 是表达式,填进坑里,展开成:

rust

5 * 5

expr 的意思是“这里必须是表达式”。填别的东西,编译报错。

四、坑的类型:常用说明符

挖坑的时候要说明“这个坑能填什么”。

说明符

能填什么

例子

expr

表达式

1 + 2、foo()

ident

名字

name、foo

ty

类型

i32、Vec<String>

pat

模式

Some(x)、_

block

代码块

{ ... }

literal

字面量

42、"hello"

tt

几乎任何东西

万能坑,但约束最少

最常用的是 expr、ident、ty。

tt 是万能坑,什么都能填。但因为它什么都不限制,出错时提示很模糊。

能用具体的就用具体的,别偷懒用 tt。

五、多条规则:从上往下试

一个宏可以写多条规则,像 match 一样,从上往下匹配。

rust

macro_rules! my_vec {    () => {        Vec::new()    };    ($($x:expr),*) => {        vec![$($x),*]    };}fn main() {    let empty: Vec<i32> = my_vec![];    let nums = my_vec![1, 2, 3];    println!("{:?}", empty); // []    println!("{:?}", nums);  // [1, 2, 3]}

第一条规则:什么都没填,返回空 Vec。

第二条规则:填了东西,用 vec! 包起来。

编译器先试第一条,不匹配再试第二条。

六、可变参数:$() 和 *

这是宏最厉害的地方:参数数量可以不固定。

rust

macro_rules! my_vec {    ($($x:expr),*) => {        {            let mut v = Vec::new();            $(v.push($x);)*            v        }    };}

这段看起来吓人,其实就两个新东西:

$($x:expr),*

意思是:零个或多个表达式,用逗号隔开。

* $( 和 ) 把重复的部分包起来。

* $x:expr 是每次重复要抓的东西。

* , 是分隔符。

* * 表示“零个或多个”。

最后组合起来解读:就是建立一条传送带 $(),上面放一台抓取机(抓表达式 $x)和一个逗号 ,。然后按下无限循环开关 *。只要用户输入的东西符合‘表达式+逗号’的规律,你就一直抓、一直抓,直到抓完为止。

$(v.push($x);)*

意思是:对每个抓到的 $x,生成一条 v.push(...)。

同样是 $() 包起来,后面 * 表示重复。

所以 my_vec![1, 2, 3] 展开成:

rust

let mut v = Vec::new();v.push(1);v.push(2);v.push(3);v

*、+、? 的区别:

1. *:零个或多个。

2. +:一个或多个。

3. ?:零个或一个。

my_vec![] 用 * 能匹配,用 + 就报错,因为一个都没有。

七、一个能用的例子:max!

rust

macro_rules! max {    ($x:expr) => {        $x    };    ($x:expr, $($rest:expr),+) => {        {            let a = $x;            let b = max!($($rest),+);            if a > b { a } else { b }        }    };}fn main() {    println!("{}", max!(1));              // 1    println!("{}", max!(3, 1, 4, 1, 5));  // 5}

逻辑:

* 只有一个参数:直接返回。

* 多个参数:拿第一个,剩下的一路递归比较。

宏可以调用自己,这是处理不定数量参数的常用办法。

八、卫生性:宏里的变量不会串

Rust 宏有个好特性:宏里定义的变量,不会和外面的变量打架。

rust

macro_rules! bad {    () => {        let x = 1;    };}fn main() {    let x = 2;    bad!();    println!("{}", x); // 2}

宏里有个 x,外面也有个 x。但宏里的 x 是独立的,不会把外面的 x 改掉,这叫卫生性。

C 语言的宏是纯文本替换,很容易出现“我明明没改这个变量,怎么值变了”的问题。Rust 从根上避免了。

九、导出宏:让别人也能用

想让别的 crate 用你的宏,加 #[macro_export]:

rust

#[macro_export]macro_rules! my_macro {    () => {        println!("hello");    };}

加了之后,别人可以:

rust

use my_crate::my_macro;my_macro!();

关键坑:用 $crate

宏里如果要引用自己 crate 里的东西,不能写 crate::,要写 $crate::。

rust

#[macro_export]macro_rules! create_user {    ($name:expr) => {        $crate::User {            name: $name.to_string(),        }    };}

为什么?

* crate:: 在用的人的 crate 里找,可能找不到。

* $crate:: 在定义宏的人的 crate 里找,永远对。

这是写导出宏时最容易踩的坑,记住就行。

十、常见坑

1. 忘了 $

rust

macro_rules! bad {    (x:expr) => { x }; // 错,应该是 $x}

挖坑和填坑都要带 $。

2. * 和 + 搞混

rust

($($x:expr),*)  // 允许零个($($x:expr),+)  // 至少一个

my_vec![] 只能用 *。

3. 展开后少分号

rust

$(v.push($x);)*  // push 是语句,必须带分号

4. 导出宏用了 crate::

改成 $crate::。

5. 用了 tt 导致报错看不懂

tt 什么都不限制,出错时编译器也说不清哪里错了。

能用 expr 就别用 tt。

6. 忘了宏名后面加 !

rust

say_hello();  // 错,这是函数调用say_hello!(); // 对

十一、小练习

1. 最简单的宏

rust

macro_rules! hello {    () => {        println!("hello");    };}fn main() {    hello!();}

2. 带参数的宏

rust

macro_rules! square {    ($x:expr) => {        $x * $x    };}fnmain() {    println!("{}", square!(5));}

3. 可变参数

rust

macro_rules! my_vec {    ($($x:expr),*) => {        {            let mut v = Vec::new();            $(v.push($x);)*            v        }    };}fn main() {    let v = my_vec![1, 2, 3];    println!("{:?}", v);}

4. 多条规则

rust

macro_rules! describe {    () => { println!("空"); };    ($x:expr) => { println!("一个: {}", $x); };    ($x:expr, $($rest:expr),+) => {        println!("多个,第一个: {}", $x);    };}fn main() {    describe!();    describe!(1);    describe!(1, 2, 3);}

5. 递归计数

rust

macro_rules! count {    () => { 0 };    ($x:expr) => { 1 };    ($x:expr, $($rest:expr),+) => {        1 + count!($($rest),+)    };}fn main() {    println!("{}", count!());        // 0    println!("{}", count!(1, 2, 3)); // 3}

十二、小结

今天只记住声明宏这几件事:

* 宏就是代码模板,编译期替换。

* macro_rules! 定义宏,名字后面调用要加 !。

* 规则左边是模式,右边是展开后的代码。

* $x:expr 挖坑,说明能填什么。

* 常用说明符:expr、ident、ty、tt。

* 多条规则从上往下匹配。

* $($x:expr),* 匹配零个或多个,用逗号分隔。

* $(...)* 对每个匹配项重复生成代码。

* * 零个或多个,+ 至少一个,? 零个或一个。

* 宏可以递归调用自己。

* 卫生性保证宏里的变量不污染外部。

* #[macro_export] 导出,导出宏里用 $crate::。

一句话总结:

宏就是编译期的代码模板。macro_rules! 定义,$x:expr 挖坑,$() 处理重复,多条规则从上往下试,递归处理不定数量参数。导出用 #[macro_export],引用自己用 $crate::。

下一篇,我们讲 宏:过程宏入门。

我们下篇见。

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料