Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >rust声明式宏

rust声明式宏

作者头像
zy010101
发布于 2023-07-24 08:42:04
发布于 2023-07-24 08:42:04
37500
代码可运行
举报
文章被收录于专栏:程序员程序员
运行总次数:0
代码可运行

在 rust 中,我们一开始就在使用宏,例如 println!, vec!, assert_eq! 等。看起来宏和函数在使用时只是多了一个 !。实际上这些宏都是声明式宏(也叫示例宏或macro_rules!),rust 还支持过程宏,过程宏为我们提供了强大的元编程工具。

声明式宏

声明式宏类似于 match 匹配。它可以将表达式的结果与多个模式进行匹配。一旦匹配成功,那么该模式相关联的代码将被展开。和 match 不同的是,宏里的值是一段 rust 源代码。所有这些都发生在编译期,并没有运行期的性能损耗。下面是一个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 声明一个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 的方式来查看一下预处理阶段之后的代码。cargo-expand 正好提供了相应的功能。使用 cargo 安装 cargo-expand 即可。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
cargo install cargo-expand

安装 cargo-expand 之后,可以使用 cargo expand 命令来查看声明式宏是如何被展开的。上面的代码在执行cargo expand之后输出如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#![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 的右边都被展开了,并且如果传入的参数是一个表达式,则会将整个表达式作为一个整体传递给宏。这就是某些地方提到的“Hygienic Macros”(有些地方也翻译为卫生宏,翻译的很抽象)。最后一行代码中传入的b+3被当做了一个整体。如果是在C/C++中,不会自动将表达式作为整体,而是直接进行字符串替换。而 Rust 编译器会自动处理变量名和作用域,确保宏展开后的代码不会引入未预料的变量冲突。下面是一个C/C++中使用宏的例子。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#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 来获取预处理之后的代码。由于展开之后的代码非常得多,我们只放上 main 函数中展开的部分。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int main() {
    int a = 10;
    int b = 22;
    int _res = a + b;
    _res = a+1 + b;
    _res = a*2 + b+3;
}

可以看到,调用的代码展开之后,并没有将 b+3 作为一个整体来处理,而是简单的进行替换。因此,我们在 C/C++ 中编写宏要特别注意,宏参数在使用的时候必须加上括号。现在我们来修复上面 C/C++ 代码中的宏。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#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)
} 

这样,我们在使用宏的时候,就避免了意外结果的发生。这样展开之后的代码如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int main() {
    int a = 10;
    int b = 22;
    int _res = (a) + (b);
    _res = (a+1) + (b);
    _res = (a*2) + (b+3);
}

我们接着来定义我们自己的 my_vec! 宏, 来对声明式宏的相关语法做一个解释。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
macro_rules! my_vec {
    // 匹配 my_vec![]
    () => {
        std::vec::Vec::new()
    };
    // 匹配 my_vec![1,2,3]
    ($($el:expr), *) => {
        // 这段代码需要用{}包裹起来,因为宏需要展开,这样能保证作用域正常,不影响外部。这也是rust的宏是 Hygienic Macros 的体现。 
        // 而 C/C++ 的宏不强制要求,但是如果遇到代码片段,在 C/C++ 中也应该使用{}包裹起来。
        {
            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)
    };
}
  1. 由于宏要在调用的地方展开,我们无法预测调用者的环境是否已经做了相关的 use,所以我们使用的代码最好带着完整的命名空间。
  2. 在声明宏中,条件捕获的参数使用 开头的标识符来声明。每个参数都需要提供类型,这里 expr 代表表达式,所以 el:expr 是说把匹配到的表达式命名为 el。(...),* 告诉编译器可以匹配任意多个以逗号分隔的表达式,然后捕获到的每一个表达式可以用 el 来访问。由于匹配的时候匹配到一个 (...)* (我们可以不管分隔符),在执行的代码块中,我们也要相应地使用 (...)* 展开。所以这句 (v.push(el);)* 相当于匹配出多少个 el 就展开多少句 push 语句。
  3. 如果传入用冒号分隔的两个表达式,那么会用 from_element 构建 Vec。

我们来使用一下自定义的 my_vec! 宏

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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}

这段宏调用,展开以后,如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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]; 被展开为

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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宏的定义一样。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// println宏的定义
macro_rules! println {
    () => {
        $crate::print!("\n")
    };
    ($($arg:tt)*) => {{
        $crate::io::_print($crate::format_args_nl!($($arg)*));
    }};
}

可以看到,println带有参数将会使用 format_args_nl! 宏,但是expand确是 format_args 宏。大概可能是因为文档中说format_args_nl宏是nightly模式下的吧!并没有完全展开是因为该宏是内置宏(rustc_builtin_macro)。

在使用声明宏时,我们需要为参数明确类型,刚才的例子都是使用的expr,其实还可以使用下面这些:

  • item,比如一个函数、结构体、模块等。
  • block,代码块。比如一系列由花括号包裹的表达式和语句。
  • stmt,语句。比如一个赋值语句。
  • pat,模式。
  • expr,表达式。刚才的例子使用过了。
  • ty,类型。比如 Vec。
  • ident,标识符。比如一个变量名。
  • path,路径。比如:foo、::std::mem::replace、transmute::<_, int>。 meta,元数据。一般是在 #[...]`` 和 #![…]`` 属性内部的数据。
  • tt,单个的 token 树。
  • vis,可能为空的一个 Visibility 修饰符。比如 pub、pub(crate)

声明式宏还算比较简单。它可以帮助我们解决一些问题。

  1. 代码重复:声明式宏可以帮助消除代码中的冗余,通过将重复的代码逻辑抽象成宏,从而减少代码量并提高代码的可读性和维护性。
  2. 代码模板化:宏可以用于定义代码模板,允许在编译时根据不同的参数生成特定的代码片段,从而实现代码的泛化和重用。
  3. 实现函数重载,宏可以匹配多种模式的参数来实现函数重载。

宏的缺点

宏目前的编写无法得到IDE很好的支持,另外一点就是如无必要,就不要编写宏。如果要编写,那么尽量编写声明式宏,而不是过程宏。

  1. 宏编写复杂:过程宏的编写可能相对复杂,特别是对于复杂的语法分析和代码生成任务,编写和调试过程宏可能需要更多的时间和精力。
  2. 可读性下降:宏可能会导致代码的可读性下降,特别是在宏的展开代码复杂或嵌套层级较多时,代码可读性可能变差。
  3. 不利于错误检查:宏展开发生在编译期间,因此错误信息可能不够明确和直观,难以定位宏展开后的具体错误位置。
  4. 难以调试:宏展开过程对于开发者不是透明的,因此在调试过程中可能会遇到难以解决的问题。

参考资料

  1. https://github.com/rust-lang/rust/issues/93904
  2. https://blog.logrocket.com/macros-in-rust-a-tutorial-with-examples/#:~:text=Declarative%20macros%20enable%20you%20to,Rust%20code%20it%20is%20given.
  3. rust编程第一课-陈天
  4. The Little Book of Rust Macros
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-07-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【Rust 基础篇】Rust宏:代码生成的黑魔法
Rust是一门以安全性和性能著称的系统级编程语言,它提供了强大的宏系统,使得开发者可以在编译期间生成代码,实现元编程(Metaprogramming)。宏是Rust中的一种特殊函数,它可以接受代码片段作为输入,并根据需要生成代码片段作为输出。本篇博客将深入探讨Rust中的宏,包括宏的定义、宏的分类、宏的使用方法,以及一些实际场景中的应用案例,以便读者全面了解Rust宏的神奇之处。
繁依Fanyi
2023/10/12
1.3K0
Rust中的过程宏
Rust 吉祥物是只螃蟹,Ferris,这可以理解,但是它为什么被煮了啊?都变红了。
杨永贞
2022/06/27
2.6K0
Rust中的过程宏
【Rust 基础篇】Rust 声明宏:代码生成的魔法
Rust是一门以安全性和性能著称的系统级编程语言,它提供了强大的宏系统,使得开发者可以在编译期间生成代码,实现元编程(Metaprogramming)。宏是Rust中的一种特殊函数,它可以接受代码片段作为输入,并根据需要生成代码片段作为输出。本篇博客将深入探讨Rust中的声明宏,包括声明宏的定义、声明宏的特点、声明宏的使用方法,以及一些实际场景中的应用案例,以便读者全面了解Rust声明宏的魔力。
繁依Fanyi
2023/10/12
4860
Rust入坑指南:万物初始
有没有同学记得我们一起挖了多少个坑?嗯…其实我自己也不记得了,今天我们再来挖一个特殊的坑,这个坑可以说是挖到根源了——元编程。
Jackeyzhe
2020/04/10
1.3K0
Rust入坑指南:万物初始
Rust中打印语句为什么使用宏实现?
在Rust中,打印语句使用宏(例如println!和format!)的主要原因是为了在编译时进行字符串格式检查,并在不引入运行时开销的情况下提供更高的性能和安全性。宏可以被多次调用,这样你可以在不同的地方重复使用相同的代码模式。这有助于减少代码重复,提高代码的可维护性。
程序饲养员
2024/02/15
3010
Rust中打印语句为什么使用宏实现?
官宣 Rust 2021 Edition 计划 一睹为快
原文: The Plan for the Rust 2021 Edition[1]
张汉东
2021/05/11
2.2K0
Rust语法入门
Rust 是一种系统级编程语言,它的设计目标是提供高性能、安全性和并发性。Rust 的主要优势包括:
码客说
2023/04/17
1.3K0
听GPT 讲Rust源代码--compiler(47)
在Rust源代码中,rust/compiler/rustc_builtin_macros/src/format_foreign.rs这个文件的作用是处理外部格式化宏的实现。这些宏是Rust语言用来格式化输出的宏,它们在编译时被翻译成具体的代码实现。
fliter
2024/04/26
1330
听GPT 讲Rust源代码--compiler(47)
最强肉坦:RUST多线程
这是几乎每种编程语言都会遇到的实现场景,通过对比Java和Rust的实现与运行表现,我们可以清晰地看出Rust的不同或者说Rust的良苦用心,以及为了实现这一切所带来的语言特性。我们首先来看Java的实现方法。
文彬
2022/06/02
1.8K0
Rust也出2077? 最受欢迎的编程语言再度更新!
Rust语言是一种高效、可靠的通用高级语言,同时兼顾了开发效率和执行效率。Rust除了能够胜任性能敏感的任务以外,也在内存和线程安全方面有着极高的可靠性。
新智元
2021/05/28
8160
Rust也出2077? 最受欢迎的编程语言再度更新!
[Rust笔记] 规则宏的“卫生保健”
规则宏mbe即是由macro_rules!宏所定义的宏。它的英文全称是Macro By Example。相比近乎“徒手攀岩”的Cpp模板·元编程,rustc提供了有限的编译时宏代码检查功能(名曰:Mixed Hygiene宏的混合保健)。因为rust宏代码·被展开于·编译过程中的语法分析阶段(请见下图),所以rustc相较于g++/gcc拥有更多可用作“代码静态分析”的信息。
MikeLoveRust
2023/02/15
8040
[Rust笔记] 规则宏的“卫生保健”
Rust 不允许C++方式的函数重载overloading
C++方式的函数重载,即同一个函数名以及多个不同的形参类型和个数(不包括返回值类型), 以Ad-hoc(临时,随时)过于灵活的方式来实现函数的重载!功能非常强大, 同时也是惹祸根源之一!
MikeLoveRust
2020/06/28
1K0
rust写操作系统 rCore tutorial 学习笔记:实验指导零 创建项目与启动
这是 os summer of code 2020 项目每日记录的一部分: 每日记录github地址(包含根据实验指导实现的每个阶段的代码):https://github.com/yunwei37/os-summer-of-code-daily
云微
2023/02/11
1.7K0
【译】为 嵌入式 C 程序员编写的 Rust 指南
这是来自 Google OpenTitan 团队,给嵌入式 C 程序员专门打造的一份 Rust 指南。
张汉东
2021/10/13
5.3K0
Rust实战系列-基本语法
本文是《Rust in action》学习总结系列的第二部分,更多内容请看已发布文章:
abin
2023/03/21
2.3K0
Rust实战系列-基本语法
Rust生态安全漏洞总结系列 | Part 1
本系列主要是分析RustSecurity 安全数据库库中记录的Rust生态社区中发现的安全问题,从中总结一些教训,学习Rust安全编程的经验。
张汉东
2021/02/24
1.1K0
源码阅读 | 第一期 : 名称解析
首先要确保自己对 rustc_resolve 这个库的上下文信息有所了解,也就是上面提到的 编程过程中的三类困扰中的第二类问题要做信息补充。第一类和第三类问题,相信对于 非 Rust 新手应该是可以避开了。一般阅读 Rust 源码最常见的问题就是第二类问题,缺乏对程序要处理问题领域的信息的了解。
张汉东
2021/11/17
1.7K1
一网打尽 Rust 语法
大家好,我是「柒八九」。一个「专注于前端开发技术/Rust及AI应用知识分享」的Coder
前端柒八九
2024/04/30
1650
一网打尽 Rust 语法
为什么Rust的println!不会发生所有权转移?
println!可能是学习Rust最常用的一行代码了。我们连续多次调用它,下面的代码编译通过,再正常不过了。
袁承兴
2020/10/10
1.4K1
为什么Rust的println!不会发生所有权转移?
Rust 写脚手架,Clap你应该知道的二三事
大家好,我是「柒八九」。一个「专注于前端开发技术/Rust及AI应用知识分享」的Coder。
前端柒八九
2024/03/18
3920
Rust 写脚手架,Clap你应该知道的二三事
相关推荐
【Rust 基础篇】Rust宏:代码生成的黑魔法
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验