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

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。

相关推荐
SomeB1oody1 天前
【Rust自学】4.1. 所有权:栈内存 vs. 堆内存
开发语言·后端·rust
SomeB1oody2 天前
【Rust自学】4.2. 所有权规则、内存与分配
开发语言·后端·rust
SomeB1oody2 天前
【Rust自学】4.5. 切片(Slice)
开发语言·后端·rust
编码浪子2 天前
构建一个rust生产应用读书笔记6-拒绝无效订阅者02
开发语言·后端·rust
baiyu332 天前
1小时放弃Rust(1): Hello-World
rust
baiyu332 天前
1小时放弃Rust(2): 两数之和
rust
Source.Liu2 天前
数据特性库 前言
rust·cad·num-traits
编码浪子2 天前
构建一个rust生产应用读书笔记7-确认邮件1
数据库·rust·php
SomeB1oody2 天前
【Rust自学】3.6. 控制流:循环
开发语言·后端·rust
Andrew_Ryan2 天前
深入了解 Rust 核心开发团队:这些人如何塑造了世界上最安全的编程语言
rust