精通rust宏系列教程-入门篇

embedded/2024/11/19 7:19:22/

Rust最令人敬畏和强大的特性之一是它使用和创建宏的能力。不幸的是,用于创建宏的语法可能相当令人生畏,并且对于新开发人员来说,这些示例可能会令人不知所措。我向你保证Rust宏非常容易理解,本文将为你介绍如何创建自己的宏。

什么是Rust宏?

rust">println!("hello {}", name)

如果你已经尝试过Rust,你应该已经使用过一个宏println!这个宏允许您打印一行文本,并能够在文本字符串中插入变量。

宏只是允许你发明自己的语法并编写代码生成更多的代码。这被称为元编程,支持语法糖,使你的代码更短,更容易使用你的库,甚至可以在rust中创建自己的DSL(领域特定语言)。
在这里插入图片描述

宏的工作方式是匹配宏规则中定义的特定模式,将匹配的部分捕获为变量,然后展开以生成更多代码。如果你听不懂也没关系。让我们开始吧!

如何创建宏

我们可以使用macro_rules!宏。Macroception !

这就是如何创建一个空白嘿!宏;很明显,它现在没有任何作用。

rust">macro_rules! hello {() => {}
}

()=>{}部分似乎很有趣,不是吗?

这是一个宏规则的条目,我们可以在一个宏中有许多规则来匹配它。这与模式匹配非常相似,在模式匹配中,我们也可以有许多用逗号分隔的情况。

但是,它到底是什么意思呢?

()=>{}
匹配器编写器
匹配模式扩展代码

括号部分是匹配器,它将允许我们匹配模式并捕获其中一部分作为变量。这就是我们如何发明自己的自定义语法和dsl的方法。

花括号部分是转录器,在这里我们可以使用从匹配器捕获的变量。Rust编译器会将宏的代码及其变量扩展为实际的Rust代码。

匹配模式

我们如何匹配一个模式呢?让我们来看看。

($name:expr)
  • $name 将在后面编写器引用
  • 指示符:用于匹配类型,如:expression(表达式类型)

Rust将尝试匹配在匹配器对中定义的模式,匹配器对可以是()、{}或[]。宏规则中的字符将与输入进行匹配,以确定是否匹配。在规则的模式匹配成功之后,我们可以捕获模式的一部分,并将其捕获为一个变量,以便在编写器中使用。

美元符号之后的部分是变量名称,它将在编写器中作为变量使用,这就是我们的代码所在的位置。在本例中,我们当前将其捕获为$name。

冒号后面的部分称为指示符,是我们可以选择匹配的类型。例如,我们目前使用的是表达式指示符,由冒号后面的expr表示。这告诉Rust匹配一个表达式,并将其捕获为$name。

我们可以使用许多指示符,而不仅仅是表达式。下面是Rust中可用的指示符的快速列表:

  • 标识符(Identifiers)

说明:标识符是用来表示变量、函数、结构体等的名称。在声明性宏中,标识符参数用于匹配和捕获代码中的名称,然后可以在宏展开时对这些名称进行操作。

rust">macro_rules! print_variable_name {($var:ident) => {println!("The variable name is: {}", stringify!($var));};
}
let x = 5;
print_variable_name!(x);
  • 字面量(Literals)

说明:字面量包括整数字面量、浮点数字面量、字符字面量和字符串字面量等。在宏中,字面量参数用于匹配特定类型的常量值。

rust">-- 数值字面量
macro_rules! double_number {($num:literal) => {$num * 2};
}
let result = double_number!(5);
assert_eq!(result, 10);-- 字符串字面量
macro_rules! print_greeting {($name:literal) => {println!("Hello, {}!", $name);};
}
print_greeting!("Alice");
  • 路径(Paths)

说明:路径用于指定一个类型、函数或者模块的位置。在宏中,路径参数可以用来匹配和操作代码中的类型或函数引用。

rust">macro_rules! call_function {($func_path:path) => {$func_path();};
}
fn my_function() {println!("This function is called through a macro.");
}
call_function!(my_function);

$func_path:path是路径参数。这里宏call_function接受函数路径作为参数,然后调用该函数。

  • 表达式(Expressions)

说明:表达式是可以计算出值的代码片段。在宏中,表达式参数用于匹配和操作代码中的各种表达式,如算术表达式、函数调用表达式等。

rust">macro_rules! square_expression {($expr:expr) => {($expr) * ($expr)};
}
let x = 3;
let result = square_expression!(x + 1);
assert_eq!(result, 16);

square_expression接受一个表达式作为参数,然后将这个表达式自身相乘,计算出表达式值的平方。

  • 类型(Types)

说明:类型参数用于匹配和操作代码中的数据类型。可以在宏中根据不同的类型进行不同的代码展开。

rust">macro_rules! print_type_name {($ty:ty) => {println!("The type is: {}", stringify!($ty));};
}
print_type_name!(u32);

$ty:ty是类型参数。宏print_type_name接受类型作为参数,然后打印出这个类型的名称(通过stringify!宏转换为字符串)

  • 块(Blocks)

说明:块是由花括号包围的一系列语句。在宏中,块参数用于匹配和操作代码中的语句块,这样可以在宏展开时对整个语句块进行处理。

rust">macro_rules! execute_block {($block:block) => {$block};
}
let result = execute_block!({let x = 5;x * 2
});
assert_eq!(result, 10);

execute_block接受语句块作为参数,然后执行这个语句块并返回结果。

  • token(标记)参数

token参数是一种比较通用的参数类型,它可以匹配几乎任何语法结构单元。这包括但不限于关键字、操作符、标点符号等。使用token参数可以让宏更灵活地处理各种复杂的语法模式。

rust">macro_rules! handle_tokens {($first_token:token $second_token:token) => {println!("The first token is: {:?}, the second token is: {:?}", $first_token, $second_token);};
}
handle_tokens!(let +);

$first_token:token$second_token:token用于匹配任意两个连续的语法标记。这个宏会打印出这两个标记的内容。

  • meta(元数据)参数

meta参数用于匹配和处理属性(attribute)相关的语法结构。在 Rust 中,属性用于为各种语言元素(如函数、结构体、枚举等)添加额外的元数据,如条件编译信息、派生(derive)的 trait 等。

rust">macro_rules! print_meta {($meta:meta) => {println!("The meta data is: {:?}", $meta);};
}
#[derive(Debug)]
struct MyStruct;
print_meta!(#[derive(Debug)]);

$meta:meta用于匹配属性语法结构。这个宏可以打印出属性的内容,就像上面打印#[derive(Debug)]这个属性一样。

第一个宏示例

太酷了! 现在我们知道了创建简单宏的足够内容。让我们创造属于我们自己的hello!宏。

rust">macro_rules! hello{($name:expr) => {println!("hello {}!", $name);};
}fn main(){hello!("Rust");
}

就是这样,我们刚刚创建了第一个宏!这个程序会打印出 hello Rust!作为调用宏的结果。我们匹配了输入表达式,并将其捕获为$name变量,然后在编写器中使用捕获的$name,它将由Rust编译器展开。这看起来很简单,不是吗?

第二个宏示例

我们所熟悉和喜爱的许多宏可以一次接受大量输入。vec!宏就是典型例子;我们可以通过调用vec!宏像这样:vec![1,2,3,4,5]。vec!宏如何实现的呢,我们来慢慢分解说明。

( $( $x:expr),* )这些*号 会在$(...) 重复多次, 逗号是分隔符; 

我们只需将想要重复的模式放在$(…)部分中。然后,插入分隔符,在本例中是逗号(,)符号。这将是一个字符,将模式分开,让我们有重复。

最后在末尾添加星号(*)符号,它将重复匹配$()中的模式,直到匹配完成为止。困惑吗?让我们再看一个例子吧!

在这种情况下,hello!宏,我们捕捉所有的输入表达式作为$name,我们将继续匹配它,直到没有匹配。我们可以用任意多的参数调用这个宏。

仍然困惑吗?这完全没问题!让我们实现一个令人敬畏的现实世界的宏,看看它是如何工作的。

  • 实现Rust HashMap
rust">use std::collections::HashMap;macro_rules! mapx {( $( $key:expr=>$value:expr ),* ) => {{let mut hmap = HashMap::new();$(hmap.insert($key, $value);)*hmap}};
}fn main() {let addr_maps = mapx!("a1"=>"beijing01", "a2"=>"beijing02","a3"=>"beijing03"
);println!("{:?}", addr_maps);
}
// 输出结果:
// {"a1": "beijing01", "a2": "beijing02", "a3": "beijing03"}
  • 定义了宏 mapx!,它接受一系列以 => 分隔的键值对表达式作为参数。
  • 在宏展开时,首先创建一个新的空 HashMap,然后通过循环遍历传入的键值对,将每个键值对插入到 HashMap 中,最后返回初始化好的 HashMap

这样就可以方便地一次性初始化多个 HashMap 的键值对,类似于 vec! 宏用于初始化向量的便捷方式。

这里需要注意的是,这段展开代码使用 {} 括起来, 虽然外面已经有 {};,下面我们分析下:

rust">        {let mut hmap = HashMap::new();$(hmap.insert($key, $value);)*hmap}
  1. 作用域和变量遮蔽问题
    • 在宏展开的环境中,使用{}创建一个新的内部块作用域是很重要的。虽然外层可能有其他的块作用域(比如main函数本身就是一个块作用域),但这里的内部块主要是为了确保变量的正确生命周期和作用范围。
    • 如果没有这个内部块,在宏展开时可能会出现变量遮蔽(shadowing)等问题。例如,如果在宏调用的上下文中已经存在一个名为hmap的变量,那么没有内部块的话,宏内部对hmap的操作可能会意外地影响到外部的hmap变量。
  2. 返回值的明确性
    • 这个内部块最后返回hmap,明确了宏的返回值是这个新创建并初始化好的HashMap。从语法角度看,在 Rust 的表达式导向的语法中,一个块({})可以作为一个表达式,它的值是最后一个表达式的值(在这里就是hmap的值)。如果没有这个块,宏展开后的代码在语法上可能不符合期望的返回值要求,导致编译错误或者意外的行为。

最后总结

本文介绍了Rust宏,主要了解宏的宏的工作方式,如何匹配宏规则中定义的特定模式,将匹配的部分捕获为变量,然后展开以生成更多代码。我们通过几个简单示例,你应该大概清楚了创建宏的过程。当然要掌握Rust宏,还有很长的路要走,如何编写声明宏、过程宏等,未来我们继续,一起rust


http://www.ppmy.cn/embedded/138721.html

相关文章

【星海随笔】分布式管理Zookeeper

高可用集群 地址:https://archive.apache.org/dist/zookeeper TPS既每秒系统吞吐量 QPS即每秒查询率 Zookeeper的选举机制 确保所有节点对外表现为一个统一的服务。 选举机制分为两个阶段:Leader选举和投票确认 Zookeeper 的选举机制确保集群中的所有节…

C# OpenCV 通过高度图去筛选轮廓

//输入图像 threshCropMap.ImWrite("D:\\test\\threshCropMap_BeforeFilterByBlob.bmp"); //设定我们要筛选的高度 var ResultHeight 60; //创建对应高度的图像,由于是高度信息图,所有要使用32位来存放数据 Mat mat new Mat(filter.Rows, fi…

单片机学习笔记 4. 蜂鸣器滴~滴~滴~

更多单片机学习笔记:单片机学习笔记 1. 点亮一个LED灯单片机学习笔记 2. LED灯闪烁单片机学习笔记 3. LED灯流水灯 目录 0、实现的功能 1、Keil工程 1-1 蜂鸣器工作原理 1-2 三极管工作原理 1-3 蜂鸣器原理图 2、代码实现 0、实现的功能 使蜂鸣器滴~滴~滴~ 1…

ue5入门教程:Pawn

一、Pawn的基本概念 定义:Pawn是由玩家或AI控制的所有Actor的基类。它包含了物理实体、移动和控制这三个核心要素。关系:默认情况下,控制器(Controller)和Pawn之间是一对一的关系。控制器负责处理玩家的输入&#xff…

无人机在森林中的应用!

一、森林资源调查 无人机可以利用遥感技术快速获取所需区域高精度的空间遥感信息,对森林图斑进行精确区划。相较于传统手段,无人机调查具有低成本、高效率、高时效的特点,尤其在地理环境条件不好的区域,调查人员无法或难以到达的…

分布式(Hadoop\Spark)

一、分布式 1、基本概念 分布式计算是一种计算模型,其中任务被分解为较小的子任务,这些子任务在多个计算节点(如计算机或服务器)上并行执行,以加快处理速度和提高计算能力。分布式计算的目标是通过协调这些节点来解决…

(五)自定义组件

(五)自定义组件 1、 VS插件推荐2、开始创建自定义的组件2.1、 快速添加基础页面内容: vbase2.2、 随便写上内容 3、使用该组件3.1、具体步骤3.2、其他说明 1、 VS插件推荐 开始前,如果大家使用的是VS Code,我推荐安装Vue VSCode S…

C# 异常处理、多个异常、自定义异常处理

C# 异常 异常是为处理异常的发生而设计的,这些特殊情况会改变程序执行的正常流程。 引发或引发异常。 在执行应用期间,许多事情可能出错。 磁盘可能已满,我们无法保存文件。 当我们的应用尝试连接到站点时,Internet 连接可能会断…