广告位联系
返回顶部
分享到

一文弄懂rust声明宏

Rust语言 来源:互联网 作者:佚名 发布时间:2024-03-31 22:04:38 人浏览
摘要

Rust支持两种宏,一种是声明宏,一种是过程宏,前者相较于后者还是比较简单的。本文主要是讲解Rust元编程里的声明宏,通过声明宏可以减少一些样板代码,它是一个用代码生成代码的技术。

Rust支持两种宏,一种是声明宏,一种是过程宏,前者相较于后者还是比较简单的。本文主要是讲解Rust元编程里的声明宏,通过声明宏可以减少一些样板代码,它是一个用代码生成代码的技术。

声明宏的主要原理是通过匹配传入的代码然后替换成指定的代码,因为替换是发生在编译器,所以rust的宏编程没有任何运行时的开销,可以放心的用,不用担心性能 :)。

快速入门

声明宏不像过程宏那样需要在单独的包(package/crate)中定义,只需要使用macro_rules!就可以简单的定义一个声明宏,一个简单的示例如下。

1

2

3

4

5

6

7

8

9

10

11

// 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

上面这个结果应该不会让人意外,你会发现声明宏定义的那一段代码和普通的match代码非常相似,不同的在于变量前面多了个前缀$, 而且需要通过冒号:注明变量的类型,这里的变量类型是expr,这是表达式的意思。

声明宏语法

一个声明宏大致可以分为三个部分

  • 声明宏的名称定义,比如例子中的add
  • 模式匹配部分, 比如例子中的($a:expr, $b:expr)
  • 声明宏返回的部分, 也就是花括号被包裹的部分, 比如例子中的$a + $b

本文的开头说过,过程宏的原理就是通过匹配传入的代码然后替换成指定的代码, 所以上面的例子在编译(展开)之后应该会变成下面的代码。

1

2

3

4

fn main() {

    let sum = 1 + 2;

    println!("sum: {sum}");

}

如果我们传递三个参数呢? 比如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)却传入了三个变量,所以匹配不了,那么就会报错,因为这是不合法的语法。
那么,怎么匹配三个变量,或者是一个变量呢? 有两个办法,一是一一对应,二是使用重复的匹配方法。为了简单起见,我们先使用比较笨的方法,代码如下。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

macro_rules! add {

    // 声明宏的第一条匹配规则

    ($a: expr) => {

        $a

    };

    // 声明宏的第二条匹配规则

    ($a:expr, $b:expr)=>{

        $a + $b

    };

    // 声明宏的第三条匹配规则

    ($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}");

}

上面的代码和快速入门的例子没有太大的区别,主要的区别是之前的例子只有一个匹配规则,而新的例子有三条匹配规则,当rust编译代码的时候,会将调用声明宏的输入参数从上至下依次匹配每条规则,当匹配到就会停止匹配,然后返回对应的代码,这和rust的match模式匹配没有太大的区别,唯一的区别可能是, 声明宏使用;分隔不同的匹配模式,而match的不同匹配模式使用,分隔。

上面的代码输出如下:

sum1: 1
sum2: 3
sum3: 3

这样的结果并不让人意外,唯一让人沮丧的是,每种情况都写一个对应的表达式的话,得累死去。

元变量

现在让我们继续看看rust的声明宏支持哪些类型。

  • item: 条目,比如函数、结构体、模组等。
  • block: 区块(即由花括号包起的一些语句加上/或是一项表达式)。
  • stmt: 语句
  • pat: 模式
  • expr: 表达式
  • ty: 类型
  • ident: 标识符
  • path: 路径 (例如 foo, ::std::mem::replace, transmute::<_, int>, …)
  • meta: 元条目,即被包含在 #[...]及#![...]属性内的东西。
  • tt: 标记树

大多数情况,一般只会使用expr和tt, 使用expr是因为rust中几乎可以被称为基于表达式的编程语言,因为它的表达式概念非常大,即使是if和while这样的语句也可以作为一个表达式返回值,而tt是一个万金油,它可以简单的被认为是其他类型都不匹配的情况下的兜底类型。
下面看一个tt类型的例子。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

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

代码展开后长这样:

值得注意的是: 下面的代码是手动的展开,与真实的编译代码还是有点区别的!!!

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

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)

如果我真的能够手动展开自己的代码,那就肯定会了,也就不用开文章学习了不是,所以如果吃不准宏展开之后的结果或者故障排查的时候可以使用cargo expand命令查看展开后的代码。
可以通过以下命令安装。

1

cargo install cargo-expand

安装之后在项目的根目录执行cargo expand即可,上面的例子展开之后如下。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

#![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));

    };

}

如果看不太懂可以结合我手动展开的代码一起看。

标记树撕咬机(TT muncher)

通过标记树撕咬机(TT muncher)我们可以实现递归的声明宏,不过在此之前让我们先解决不定参数的问题,之前解决的方案是根据要传的参数编写声明宏的匹配代码,这样实在是太不优雅了,让我们看看怎么一次性搞定。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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表达式一样,这个不需要太纠结。

下面来看看这种模式的语法定义,重复的一般形式是$ ( ... ) sep rep

  • $ 是字面标记。
  • ( ... ) 代表了将要被重复匹配的模式,由小括号包围。
  • sep是一个可选的分隔标记。常用例子包括,和;。
  • rep是重复控制标记。当前有两种选择,分别是* (代表接受0或多次重复)以及+ (代表1或多次重复)。目前没有办法指定“0或1”或者任何其它更加具体的重复计数或区间。

大家可以将($($a: expr),*)改成($($a: expr);*),然后就会发现编译不过了,因为分隔符需要是;了

也就是说, $($a: expr),*匹配到了(), (1), (1,2),(1,2,3),为啥能匹配到()?, 因为*能匹配0个或多个,所以零参数的()也能匹配上,如果你将这个例子中的*换成+,就会发现add!()会报错,因为+要求至少一个参数。

下面以参数(1,2,3)的例子再深入一下宏展开时的操作,当传入(1,2,3)时,因为跟$($a: expr),*能够匹配上, 所以(1,2,3)里的冒号,被$($a: expr),*的冒号,给匹配上,而$a代表1 2 3中的每个元素, 那么怎么在返回的代码中标识重复的参数呢?rust的语法是, 我们需要使用$()*将$a包裹起来,外面的包装代码对应参数匹配时的重复次数, 你可以简单的将$()*认为是必要的语法。

下面看一个简单的例子

1

2

3

4

5

6

7

8

9

macro_rules! print {

    ($($a: expr),*) => {

        println!("{} {}", $($a),*)

    };

}

 

fn main() {

    print!(1,2);

}

$($a),*会原封不动的将参数放在它对应的位置,因为println!指定了两个位置参数,所以使用自定义的print只能传递两个参数。

最后看看上面那个add!宏的例子, add!(1,2,3)展开之后应该变成下面这样。

0+1+2+3

之所以这样,是因为我们在返回的代码模式中$($a)*在$a前面加了一个+, 而这个加号+因为被$()*包裹,所以会跟着$a重复一样的次数,也就变成了+1+2+3。
为啥前面要加个0?因为不加0的话, 就不是合法的表达式了。

递归示例1

虽然add!这个宏可以使用一个模式匹配就能完成,但是我们可以使用更加复杂的方式实现,也就是标记树撕咬机(TT muncher)。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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)**的代码和之前的代码结果没有什么区别,但是展开的过程中会有些不同,因为后者使用了递归,它的递归调用类似于add!(1, add!(2, add!(3, add!(3, add!(3, add!(5))))));

这段代码的前两个匹配模式不用过多介绍,关键在于最后一个($a: expr, $($other: tt)*), $a 和 ,会吃掉一个参数和一个逗号,, 而$($other: tt)*会匹配到后面所有的参数2,3,4,5。

注意这些参数包含逗号,, 还有就是我们在使用$($other: tt)*这种重复模式的时候没有指定分隔符, 所以tt既匹配了参数2 3 4 5也匹配了分割这些数字的逗号,, 所以在展开的代码$a + add!($($other)*)会变成1 + add!(2,3,4,5), 然后就是不断的递归了,直到遇到第一个匹配模式。

递归示例2

你可能在上一个例子不能感受到**标记树撕咬机(TT muncher)**的威力,所以我们继续看下一个例子。
我们可以通过**标记树撕咬机(TT muncher)**的递归调用来生成对嵌套对象的递归调用,这样就不需要不断的判断Option的值是Some还是None了。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

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:}"#);

    }

}

这个例子写完,我才发现serde_json可以直接使用["key21"]["key21"]["key23"]这样的语法直接判断!!!, 不过serde_json的返回结果都是null, 如果键值对不存在的话。

总结

我感觉rust的宏编程还是很有意思的,不过这东西的确得真正有需求的时候才会真的理解,我之前也不是太懂,看了视频和文章也不是太懂,只是知道它能干啥,但是没有一个真正要解决的问题,所以一直不能很好的掌握,直到在使用serde_json时遇到嵌套的数据结构需要写重复的判断代码时,我才在应用的时候掌握了声明宏(虽然最后发现它的实用价值可能不是那么大),至于过程宏,可能等我遇到需要过程宏的时候才会很好的掌握吧,到时候在写对应的文章吧。

参考链接

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/


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 :
相关文章
  • 一文弄懂rust声明宏
    Rust支持两种宏,一种是声明宏,一种是过程宏,前者相较于后者还是比较简单的。本文主要是讲解Rust元编程里的声明宏,通过声明宏可以减
  • Rust文本处理快速入门
    编程过程中有许多类型的数据要处理,其中文本处理必不可少,本文主要是记录在使用Rust开发的过程中处理文本相关数据的一些代码,而文
  • Rust Aya 框架编写 eBPF 程序
    Linux 内核 6.1 版本中有一个非常引人注意的变化:引入了对 Rust 编程语言的支持。Rust 是一种系统编程语言,Rust 通过提供非常强大的编译时
  • Rust中Cargo的使用介绍
    1、cargo简介 Cargo 是 Rust 的构建系统和包管理器。?多数 Rustacean 们使? Cargo 来管理他们的 Rust 项?,因为它可以为你处理很多任务,?如构建代码
  • 深入了解Rust结构体的使用

    深入了解Rust结构体的使用
    结构体是一种自定义的数据类型,它允许我们将多个不同的类型组合成一个整体。下面我们就来学习如何定义和使用结构体,并对比元组与
  • 深入了解Rust中引用与借用的用法

    深入了解Rust中引用与借用的用法
    好久没更新 Rust 了,上一篇文章中我们介绍了 Rust 的所有权,并且最后定义了一个 get_length 函数,但调用时会导致 String 移动到函数体内部,
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计