rust声明式宏
宏
在 rust 中,我们一开始就在使用宏,例如 println!, vec!, assert_eq! 等。看起来宏和函数在使用时只是多了一个 !。实际上这些宏都是声明式宏(也叫示例宏或macro_rules!),rust 还支持过程宏,过程宏为我们提供了强大的元编程工具。
声明式宏
声明式宏类似于 match 匹配。它可以将表达式的结果与多个模式进行匹配。一旦匹配成功,那么该模式相关联的代码将被展开。和 match 不同的是,宏里的值是一段 rust 源代码。所有这些都发生在编译期,并没有运行期的性能损耗。下面是一个例子:
// 声明一个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 即可。
cargo install cargo-expand
安装 cargo-expand 之后,可以使用 cargo expand 命令来查看声明式宏是如何被展开的。上面的代码在执行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);
}
可以看到,每一个 _res 的右边都被展开了,并且如果传入的参数是一个表达式,则会将整个表达式作为一个整体传递给宏。这就是某些地方提到的“Hygienic Macros”(有些地方也翻译为卫生宏,翻译的很抽象)。最后一行代码中传入的b+3被当做了一个整体。如果是在C/C++中,不会自动将表达式作为整体,而是直接进行字符串替换。而 Rust 编译器会自动处理变量名和作用域,确保宏展开后的代码不会引入未预料的变量冲突。下面是一个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 来获取预处理之后的代码。由于展开之后的代码非常得多,我们只放上 main 函数中展开的部分。
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++ 代码中的宏。
#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)
}
这样,我们在使用宏的时候,就避免了意外结果的发生。这样展开之后的代码如下所示:
int main() {int a = 10;int b = 22;int _res = (a) + (b);_res = (a+1) + (b);_res = (a*2) + (b+3);
}
我们接着来定义我们自己的 my_vec! 宏, 来对声明式宏的相关语法做一个解释。
macro_rules! my_vec {// 匹配 my_vec![]() => {std::vec::Vec::new()};// 匹配 my_vec, *) => {// 这段代码需要用{}包裹起来,因为宏需要展开,这样能保证作用域正常,不影响外部。这也是rust的宏是 Hygienic Macros 的体现。 // 而 C/C++ 的宏不强制要求,但是如果遇到代码片段,在 C/C++ 中也应该使用{}包裹起来。{let mut v = std::vec::Vec::new();$(v.push($el);)*v}};// 匹配 my_vec => {std::vec::from_elem($el, $n)};
}
-
由于宏要在调用的地方展开,我们无法预测调用者的环境是否已经做了相关的 use,所以我们使用的代码最好带着完整的命名空间。
-
在声明宏中,条件捕获的参数使用
$
开头的标识符来声明。每个参数都需要提供类型,这里expr
代表表达式,所以$el:expr
是说把匹配到的表达式命名为$el
。$(...),*
告诉编译器可以匹配任意多个以逗号分隔的表达式,然后捕获到的每一个表达式可以用$el
来访问。由于匹配的时候匹配到一个$(...)*
(我们可以不管分隔符),在执行的代码块中,我们也要相应地使用$(...)*
展开。所以这句$(v.push($el);)*
相当于匹配出多少个$el
就展开多少句 push 语句。
反复捕获
反复捕获的一般形式是$ ( ... ) sep rep
,$ 是字面上的美元符号标记( ... ) 是被反复匹配的模式,由小括号包围。sep 是可选的分隔标记。它不能是括号或者反复操作符 rep。常用例子有 , 和 ; 。rep 是必须的重复操作符。当前可以是:1. ?:表示最多一次重复,所以此时不能前跟分隔标记。2. *:表示零次或多次重复。3. +:表示一次或多次重复。
-
如果传入用冒号分隔的两个表达式,那么会用 from_element 构建 Vec。
我们来使用一下自定义的 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);
我们在使用宏的时候,可以使用(), [], {},都是可以的。但是一般都是按照约定成俗的方式来使用。例如:vec![1,2,3]
,而不是使用 vec!{1,2,3}
。
这段宏调用,展开以后,如下所示:
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];
被展开为
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宏的定义一样。
// 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)
声明式宏还算比较简单。它可以帮助我们解决一些问题。
- 代码重复:声明式宏可以帮助消除代码中的冗余,通过将重复的代码逻辑抽象成宏,从而减少代码量并提高代码的可读性和维护性。
- 代码模板化:宏可以用于定义代码模板,允许在编译时根据不同的参数生成特定的代码片段,从而实现代码的泛化和重用。
- 实现函数重载,宏可以匹配多种模式的参数来实现函数重载。
宏的缺点
宏目前的编写无法得到IDE很好的支持,另外一点就是如无必要,就不要编写宏。如果要编写,那么尽量编写声明式宏,而不是过程宏。
- 宏编写复杂:过程宏的编写可能相对复杂,特别是对于复杂的语法分析和代码生成任务,编写和调试过程宏可能需要更多的时间和精力。
- 可读性下降:宏可能会导致代码的可读性下降,特别是在宏的展开代码复杂或嵌套层级较多时,代码可读性可能变差。
- 不利于错误检查:宏展开发生在编译期间,因此错误信息可能不够明确和直观,难以定位宏展开后的具体错误位置。
- 难以调试:宏展开过程对于开发者不是透明的,因此在调试过程中可能会遇到难以解决的问题。
参考资料
- https://github.com/rust-lang/rust/issues/93904
- 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
相关文章:
rust声明式宏
宏 在 rust 中,我们一开始就在使用宏,例如 println!, vec!, assert_eq! 等。看起来宏和函数在使用时只是多了一个 !。实际上这些宏都是声明式宏(也叫示例宏或macro_rules!),rust 还支持过程宏,过程宏为我们…...

第二章:Learning Deep Features for Discriminative Localization ——学习用于判别定位的深度特征
0.摘要 在这项工作中,我们重新审视了在[13]中提出的全局平均池化层,并阐明了它如何明确地使卷积神经网络(CNN)具有出色的定位能力,尽管它是在图像级别标签上进行训练的。虽然这个技术之前被提出作为一种训练规范化的手…...
【CSS】box-shadow 属性
box-shadow 是 CSS 属性,用于为元素添加一个阴影效果,使元素看起来浮起或有层次感。 该属性允许设置一个或多个阴影效果,其语法如下: box-shadow: h-shadow v-shadow blur spread color inset;h-shadow:水平阴影的位…...

基于深度学习的高精度课堂人脸检测系统(PyTorch+Pyside6+YOLOv5模型)
摘要:基于深度学习的高精度课堂人脸检测系统可用于日常生活中或野外来检测与定位课堂人脸目标,利用深度学习算法可实现图片、视频、摄像头等方式的课堂人脸目标检测识别,另外支持结果可视化与图片或视频检测结果的导出。本系统采用YOLOv5目标…...

Mysql错误日志、通用查询日志、二进制日志和慢日志的介绍和查看
一.日志 1.日志和备份的必要性 日志刷新 2.mysql的日志类型 (1)错误日志 查看当前错误日志和是否记录警告设置 (2)通用查询日志 查看通用查询日志的设置 (3)二进制日志 查看二进制文件的设置&…...

【Linux】Tcp服务器的三种与客户端通信方法及守护进程化
全是干货~ 文章目录 前言一、多进程版二、多线程版三、线程池版四、Tcp服务器日志的改进五、将Tcp服务器守护进程化总结 前言 在上一篇文章中,我们实现了Tcp服务器,但是为了演示多进程和多线程的效果,我们将服务器与客户通通信写成了一下死循…...
【Spring Cloud】git 仓库新的配置是如何刷新到各个微服务的原理步骤
文章目录 1. 第一次启动时2. 后续直接在 git 修改配置时3. 参考资料 本文描述了在 git 仓库修改了配置之后,新的配置是如何刷新到各个微服务的步骤 前言: 1、假设现有有 3 个微服务,1 个是 配置中心,另外 2 个是普通微服务&#x…...

三,创建订单微服务消费者 第三章
4.3 修改pom添加依赖 <dependencies><!--web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--监控--><dependency><groupId&g…...

【雕爷学编程】Arduino动手做(87)---ULN2003步进电机模组2
37款传感器与执行器的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块,依照实践出真知(一定要动手做)的理念,以学习和交流为目的&am…...

【C#】微软的Roslyn 是个啥?
一、说明 Roslyn 是微软重写的C#编译器并开源。 Roslyn 是 C# 和 Visual Basic.NET 开源编译器的代号。以下是它如何在过去十年企业Microsoft的最黑暗中开始,并成为所有C#(和VB)的开源,跨平台,公共语言引擎,…...

两个小封装电机驱动芯片:MLX813XX、A4950
一.MLX813XX MELEXIS的微型电机驱动MLX813XX系列芯片集成MCU、预驱动以及功率模块等能够满足10W以下的电机驱动。 相对于普通分离器件的解决方案,MLX813XX系列电机驱动芯片是一款高集成度的驱动控制芯片,可以满足汽车系统高品质和低成本的要…...

数据结构【绪论】
数据结构入门级 第一章绪论 什么是数据结构?什么是数据类型? 程序数据结构算法 一、基本概念: 数据:指所有能被计算机处理的,无论图、文字、符号等。数据元素:数据的基本单位,通常作为整体考…...
掌握无人机遥感数据预处理的全链条理论与实践流程、典型农林植被性状的估算理论与实践方法、利用MATLAB进行编程实践(脚本与GUI开发)以及期刊论文插图制作等
目录 专题一 认识主被动无人机遥感数据 专题二 预处理无人机遥感数据 专题三 定量估算农林植被关键性状 专题四 期刊论文插图精细制作与Appdesigner应用开发 近地面无人机植被定量遥感与生理参数反演 更多推荐 遥感技术作为一种空间大数据手段,能够从多时、多…...
Angular中组件设计需要注意什么?
在 Angular 中设计组件时,有几个重要的方面需要注意。以下是一些建议: 1、单一职责原则:确保每个组件只负责一个明确定义的任务。这有助于保持组件简单、可维护,并且易于重用。 2、组件通信:了解组件之间的通信方式。…...

电容触摸屏(TP)的工艺结构
液晶显示屏(LCM),触摸屏(TP) “GG、GP、GF”这是结构分类,第一个字母表面材质(又称为上层),第二个字母是触摸屏的材质(又称为下层),两者贴合在一起。 G玻璃,FFILM,“”贴…...
Qt小妙招:如何在可执行文件生成后,在pro文件中添加其他命令操作?
问题描述: 场景1:我的可执行文件设置生成路径为某个最终目录的bin目录下,当我要修改某些config.ini或者xxx.json,或者一些qss,css文件的时候,我想直接在构建的时候,Qtcreator帮我直接拷贝过去,…...

做好防雷检测的意义和作用
防雷检测是指对雷电防护装置的性能、质量和安全进行检测的活动,是保障人民生命财产和公共安全的重要措施。我国对防雷检测行业有明确的国家标准和管理办法,要求从事防雷检测的单位和人员具备相应的资质和能力,遵守相关的技术规范和规程&#…...

计算机启动过程uefi+gpt方式
启动过程: 一、通电 按下开关,不用多说 二、uefi阶段 通电后,cpu第一条指令是执行uefi固件代码。 uefi固件代码固化在主板上的rom中。 (一)uefi介绍 UEFI,全称Unified Extensible Firmware Interface&am…...

探索容器镜像安全管理之道
邓宇星,Rancher 中国软件架构师,7 年云原生领域经验,参与 Rancher 1.x 到 Rancher 2.x 版本迭代变化,目前负责 Rancher for openEuler(RFO)项目开发。 最近 Rancher v2.7.4 发布了,作为一个安全更新版本,也…...

【MySQL】内置函数
🌠 作者:阿亮joy. 🎆专栏:《零基础入门MySQL》 🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根 目录 👉函…...

C++实现分布式网络通信框架RPC(3)--rpc调用端
目录 一、前言 二、UserServiceRpc_Stub 三、 CallMethod方法的重写 头文件 实现 四、rpc调用端的调用 实现 五、 google::protobuf::RpcController *controller 头文件 实现 六、总结 一、前言 在前边的文章中,我们已经大致实现了rpc服务端的各项功能代…...
synchronized 学习
学习源: https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖,也要考虑性能问题(场景) 2.常见面试问题: sync出…...

Qt/C++开发监控GB28181系统/取流协议/同时支持udp/tcp被动/tcp主动
一、前言说明 在2011版本的gb28181协议中,拉取视频流只要求udp方式,从2016开始要求新增支持tcp被动和tcp主动两种方式,udp理论上会丢包的,所以实际使用过程可能会出现画面花屏的情况,而tcp肯定不丢包,起码…...

《Qt C++ 与 OpenCV:解锁视频播放程序设计的奥秘》
引言:探索视频播放程序设计之旅 在当今数字化时代,多媒体应用已渗透到我们生活的方方面面,从日常的视频娱乐到专业的视频监控、视频会议系统,视频播放程序作为多媒体应用的核心组成部分,扮演着至关重要的角色。无论是在个人电脑、移动设备还是智能电视等平台上,用户都期望…...

【网络安全产品大调研系列】2. 体验漏洞扫描
前言 2023 年漏洞扫描服务市场规模预计为 3.06(十亿美元)。漏洞扫描服务市场行业预计将从 2024 年的 3.48(十亿美元)增长到 2032 年的 9.54(十亿美元)。预测期内漏洞扫描服务市场 CAGR(增长率&…...

YSYX学习记录(八)
C语言,练习0: 先创建一个文件夹,我用的是物理机: 安装build-essential 练习1: 我注释掉了 #include <stdio.h> 出现下面错误 在你的文本编辑器中打开ex1文件,随机修改或删除一部分,之后…...

vue3+vite项目中使用.env文件环境变量方法
vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量,这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...

论文笔记——相干体技术在裂缝预测中的应用研究
目录 相关地震知识补充地震数据的认识地震几何属性 相干体算法定义基本原理第一代相干体技术:基于互相关的相干体技术(Correlation)第二代相干体技术:基于相似的相干体技术(Semblance)基于多道相似的相干体…...
Java数值运算常见陷阱与规避方法
整数除法中的舍入问题 问题现象 当开发者预期进行浮点除法却误用整数除法时,会出现小数部分被截断的情况。典型错误模式如下: void process(int value) {double half = value / 2; // 整数除法导致截断// 使用half变量 }此时...
【FTP】ftp文件传输会丢包吗?批量几百个文件传输,有一些文件没有传输完整,如何解决?
FTP(File Transfer Protocol)本身是一个基于 TCP 的协议,理论上不会丢包。但 FTP 文件传输过程中仍可能出现文件不完整、丢失或损坏的情况,主要原因包括: ✅ 一、FTP传输可能“丢包”或文件不完整的原因 原因描述网络…...