当前位置: 首页 > article >正文

【Rust自学】19.4. 宏(macro)

必须要说,这篇文章的内容你一定不可能一次性完全消化,除非你是身经百战的程序员。当然也没多大的必要完全掌握,实战中真的很少遇到要必须写宏才能解决的问题。宏的概念非常复杂,不可能是一篇文章讲得完的,这里只做了一个广却不精的介绍。如果真要系统性了解必须得依靠一个专栏的文章量。如果你真想深入了解,不妨看 The Little Book of Rust Macros。

喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
请添加图片描述

19.4.1. 什么是宏

宏(macro)在Rust里指的是一组相关特性的集合称谓:

  • 使用macro_rules!构建的声明宏(declarative macro)
  • 3种过程宏:
    • 自定义派生宏,用于structenum,可以为其指定随derive属性添加的代码
    • 类似属性的宏,在任何条目上添加自定义属性
    • 类似函数的宏,看起来像函数调用,对其指定为参数的token进行操作

19.4.2. 函数与宏的差别

  • 从本质上来讲,宏是用来编写可以生成其它代码的代码,也就是所谓的元编程(metaprogramming)
  • 函数在定义签名时,必须声明参数的个数和类型;宏可以处理可变的参数。
  • 编译器会在解释代码前展开宏
  • 宏的定义比函数复杂很多,难以阅读、理解和维护
  • 在某个文件调用宏时,必须提前定义宏或将宏引入当前作用域;函数可以在任何位置定义并在任何位置使用。

19.4.3. macro_rules!声明宏

声明宏有时候叫做宏模版,有时候叫做macro rules宏,有时候就叫做宏。

它是Rust里最常见的宏的形式,有点类似于match表达式的模式匹配,在定义声明宏时我们会用到macro_rules

看个例子:

#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

这是vec!(用于创建Vector)这个宏的简化定义版本,我们一行一行地看:

  • #[macro_export]这个标注意味着这个宏会在它所处的包被引入作用域后才可以使用,缺少了这个标注的宏就不能被引入作用域
  • macro_rules!是声明宏的关键字,这个宏的名称叫vec,后边的{}内的东西就是宏的定义体。
  • 定义体里的东西有点类似于match的模式匹配,有点像match的分支,而这里实际上只有一个分支。虽然我们一直说定义体里的东西有点类似于match的模式匹配,但它和match有本质区别:match匹配的是模式,而它匹配的是Rust的代码结构。
  • ( $( $x:expr ),* )是它的模式,后面是代码。由于这里只有一个模式,所以任何其它的模式都会导致编译时的错误。某些比较复杂的宏就可能包含多个分支。
    首先,我们使用一组括号来包含整个模式。我们使用美元符号 ($) 在宏系统中声明一个变量,该变量将包含与模式匹配的Rust代码。美元符号清楚地表明这是一个宏变量,而不是常规的Rust变量。接下来是一组括号,它们捕获与括号内的模式匹配的值,以便在替换代码中使用。 $()中是$x:expr它匹配任何 Rust 表达式,并为表达式指定名称$x*意味着这个模式能够匹配0个或是多个*之前的东西。
    假入我们写let v: Vec<u32> = vec![1, 2, 3],那么$x就会分别匹配到1、2和3上。
    现在让我们看看与该手臂相关的代码主体中的模式: $()*中的temp_vec.push()是为每个匹配$()部分生成的 在模式中出现零次或多次,具体取决于模式的次数 匹配。 $x被替换为每个匹配的表达式。当我们用vec![1, 2, 3];调用这个宏时,生成的替换该宏调用的代码如下:
{
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
}

要了解有关如何编写宏的更多信息,可以看由Daniel Keep撰写并由Lukas Wirth继续编写的“The Little Book of Rust Macros” 。

大多数程序员只是用宏而不会去编写宏,所以这部分就不深入研究了。

19.4.4. 基于属性来生成代码的过程宏

宏的第二种形式是过程宏,它的作用更像是一个函数(或者叫某种形式的过程)。过程宏接受一些代码作为输入,对该代码进行操作,并生成一些代码作为输出,而不是像声明性宏那样匹配模式并用其他代码替换代码。

一共有三种过程宏:

  • 自定义派生宏
  • 类属性宏
  • 类函数宏

创建过程宏时,定义必须单独放在其自己的包中,并且使用特殊的包类型。这是出于复杂的技术原因。Rust也在致力于消除这个要求,但起码目前还没做到。

看个例子:

use proc_macro;

#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
  • some_attribute是一个用来指定过程宏类型的占位符
  • 下面定义了过程宏的函数,接收一个TokenStream的值作为参数,产生一个TokenStream的值作为输出。
    TokenStream是在pro_macro包中定义的,它表示一段标记序列,而这也是过程宏的核心所在:需要被宏处理的源代码就组成了输入的TokenStream,而宏生成的代码则组成了输出的TokenStream
    函数附带的属性决定了我们究竟创建的是哪一种过程宏,同一个包装可以拥有多种不同类型的过程宏。

自定义派生(derive)宏

我们通过一个例子来看:

创建一个名为hello_macro的包,定义一个拥有关联函数hello_macroHelloMacro trait。我们要提供一个能自动实现trait的过程,使得用户在类型上标注#[derive(HelloMacro)]就能得到hello_macro的默认实现

首先我们需要创建一个新的工作空间(workspace),其它的项目都在工作空间之下。创建并打开Cargo.toml

touch Cargo.toml

在里面这么写:

[workspace]

members = [
	"hello_macro",
	"hello_macro_derive",
	"pancakes",
]

首先创建库crate,输入指令(注意指令应该执行在工作空间的路径下):

cargo new hellow_macro --lib

hello_macrolib.rs里写:

pub trait HelloMacro {
    fn hello_macro();
}

这么写我们就得到了一个hello_macro trait和hello_macro方法(但是没有具体实现)。

然后我们就可以在main.rs实现这个trait并为方法写上具体实现:

use hello_macro::HelloMacro;

struct Pancakes;

impl HelloMacro for Pancakes {
    fn hello_macro() {
        println!("Hello, Macro! My name is Pancakes!");
    }
}

fn main() {
    Pancakes::hello_macro();
}

这么写没问题,但是有缺点:用户希望很多类型都能使用到hello_macro功能,所以他们就必须为每一个希望使用到hello_macro功能的函数编写出类似的代码,这就非常的繁琐。

所以我们就会想使用过程宏来生成相关的代码。而且在这里面打印的话需要把类型名打印进去,它是可变的。比如说类型是Pancakes就打印"Hello, Macro! My name is Pancakes!“,如果是Apple就打印"Hello, Macro! My name is Apple!”。由于Rust没有反射,所以只能使用宏。

过程宏需要自己的库,所以在工作空间的目录下要再创建一个库crate,输入指令:

cargo new hellow_macro_derive --lib

hellow_macro_derive就是过程宏所在的crate。hellow_macro的宏写在hellow_macro_derive里是命名的惯例。

在这个crate的Cargo.toml添加(不要覆盖原本的内容!!!)这部分内容:

[lib]
proc-macro = true

[dependencies]
syn = "2.0"
quote = "1.0"

会用到synquote这两个包,所以把它们添加为依赖。

然后看一下这个crate的lib.rs怎么写:

use proc_macro::TokenStream;
use quote::quote;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Construct a representation of Rust code as a syntax tree
    // that we can manipulate
    let ast = syn::parse(input).unwrap();

    // Build the trait implementation
    impl_hello_macro(&ast)
}

fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
    let name = &ast.ident;
    let gen = quote! {
        impl HelloMacro for #name {
            fn hello_macro() {
                println!("Hello, Macro! My name is {}!", stringify!(#name));
            }
        }
    };
    gen.into()
}
  • 通过pro_macro提供的编译器接口从而在代码中读取和执行Rust代码。由于它被内置在Rust里,所以不需要把它添加为依赖项。
  • syn包是用来把Rust代码从字符转化为可供我们进一步操作的数据结构
  • quote包将syn产生的数据结构重新转化为Rust代码

这三个包使得解析Rust代码变得相当轻松。得知道,要编写一个完整的Rust代码解析器可不是一件简单的事。

简单地讲一下这里的逻辑:

  • 函数hello_macro_derive负责解析TokenStream
  • impl_hello_macro负责转换语法树(ast)

hello_macro_derive的代码在每一个过程宏的创建中都是大差不差的,不同的就是里面的impl_hello_macro。它实现的效果是用户在某个类型标注#[derive(HelloMacro)]的时候,下边的hello_macro_derive函数就会被自动地调用。

能够实现自动调用的原因是我们在定义宏时使用了#[proc_macro_derive(HelloMacro)],而且属性我们指明了是HelloMacro trait。

这个函数首先会把输入的TokenStream转化为一个可供我们解释和操作的数据结构,通过syn::parse函数把TokenStream作为输入,输出DeriveInput结构体,表示解析后的Rust代码。以上文的Pancakes类型为例,产生的输出应该是这样的:

DeriveInput {
    // ...

    ident: Ident {
        ident: "Pancakes",
        span: #0 bytes(95..103)
    },
    data: Struct(
        DataStruct {
            struct_token: Struct,
            fields: Unit,
            semi_token: Some(
                Semi
            )
        }
    )
}

ident (标识符,意思是名称)为Pancakes。其余的不细讲,详见DeriveInput官方文档。

impl_hello_macro是最后生成Rust代码的地方,返回TokenStream类型的数据。

fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
    let name = &ast.ident;
    let gen = quote! {
        impl HelloMacro for #name {
            fn hello_macro() {
                println!("Hello, Macro! My name is {}!", stringify!(#name));
            }
        }
    };
    gen.into()
}

我们使用ast.ident获得一个Ident结构实例,其中包含带注释的类型的名称(标识符)。以Pancakes类型为例,当我们对清单中的代码运行impl_hello_macro函数时, 我们得到的ident将具有值为"Pancakes"ident字段。因此,name变量将包含一个Ident结构体实例,打印时该实例将是字符串"Pancakes"

quote!宏让我们定义要返回的Rust代码。由于quote!的执行结果不能被编译器所理解,因此我们需要将其转换为TokenStream 。我们通过调用into方法来完成此操作,该方法使用此中间表示并返回所需的TokenStream类型的值。

quote!宏还提供了一些模板机制:我们可以输入#name ,然后quote!将其替换为变量中的值 name 。您甚至可以像常规宏的工作方式一样进行一些重复。详见quote官方文档。

stringify!宏内置于Rust中。它会接收Rust表达式,例如1 + 2 ,但并不会计算结果,1 + 2会被直接转化为字符串"1 + 2"并输出。这与format!或者 println!有点区别——它们计算表达式,然后将结果转换为String#name输入有可能是一个按字面值打印的表达式,所以我们使用stringify!。使用stringify!还通过在编译时将#name转换为字符串文字来保存分配。

写完这些以后来编译这两个包(使用cargo build 包名即可,注意路径哦,否则会找不到crate),然后创建一个二进制crate(一样在工作空间的目录下):

cargo new pancakes

pancakes这个crate的Cargo.toml添加(不要覆盖其他内容!!!)这些:

[dependencies]
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }

添加上hello_macrohello_macro_derive这两个依赖项。

pancakes这个crate的main.rs里这么写:

use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)]
struct Pancakes;

fn main() {
    Pancakes::hello_macro();
}

这么写就大功告成了,运行一下看看:

Hello, Macro! My name is Pancakes!

类似属性的宏

类似属性的宏又叫做属性宏。类属性宏与自定义派生宏类似,但它们不是为derive属性生成代码,而是允许你创建新属性。它们也更灵活: derive仅适用于结构和枚举;属性也可以应用于其他项目,例如函数。

下面是使用类似属性的宏的示例:

有一个名为route属性(表示路由),该属性在使用 Web 应用程序框架时注释函数

#[route(GET, "/")]
fn index() {

这个代码只是一部分,并不完整。这部分代码表示如果路径是/,方法是Get的话就会执行index这个函数。而route这个属性就是由过程宏定义的,这个宏定义的函数签名就是:

#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {

有两个TokenStream作为它的参数,attr参数对应(GET, "/")item对应函数体,也就是index函数。

除此之外,属性宏和派生宏的工作方式几乎一样,都需要建立一个pro_macro的包并提供生成相应代码的函数。

类似函数的宏

类似函数的宏又叫做函数宏。它的定义看起来像函数调用的宏。类似于macro_rules!宏,但比函数更灵活;例如,它们可以接受未知数量的参数。

函数宏可以接收TokenStream作为参数,并且它与另外两种过程宏一样,在定义中使用Rust代码来操作TokenStream

看例子:

let sql = sql!(SELECT * FROM posts WHERE id=1);

这个代码只是一部分,并不完整。我们想要定义一个能解析sql语句的宏,具体来说就是解析SELECT * FROM posts WHERE id=1,这个宏的定义就可以是:

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {

它的签名和派生宏也是比较类似的,接收一个TokenStream,返回一个相应功能的TokenStream


http://www.kler.cn/a/531218.html

相关文章:

  • MySQL子查询
  • python算法和数据结构刷题[1]:数组、矩阵、字符串
  • C++基础day1
  • 【单层神经网络】基于MXNet的线性回归实现(底层实现)
  • PHP实现混合加密方式,提高加密的安全性(代码解密)
  • 【react+redux】 react使用redux相关内容
  • Javascript代码库-jQuery入门
  • 读算法简史:从美索不达米亚到人工智能时代05天气预报
  • Apache Iceberg数据湖技术在海量实时数据处理、实时特征工程和模型训练的应用技术方案和具体实施步骤及代码
  • 爱普生L3153打印机无线连接配置流程
  • 【C++】B2120 单词的长度
  • C++11 多线程 锁与条件变量:mutex、lock_guard、unique_lock 和 condition_variable
  • 电控三周速成计划参考
  • 51c嵌入式~电路~合集25
  • GRE阅读双线阅读 --青山学堂GRE全程班 包括 阅读、数学、写作、填空、背单词
  • Math数字类
  • CH340G上传程序到ESP8266-01(S)模块
  • 大模型领域的Scaling Law的含义及作用
  • 7-4 西安距离
  • SAP HCM 回溯分析
  • 民法学学习笔记(个人向) Part.2
  • 元音字母(模拟)
  • 网络工程师 (16)侵权判断
  • DeepSeek- R1 原理介绍
  • Java中的泛型及其用途是什么?
  • windows linux常用基础命令