5 分钟速读之 Rust 权威指南(四十三)宏
宏
你有没有注意到 rust 中的函数参数都是固定数量的,而print!
宏和vec!
宏的参数数量却是任意的?这一节我们就看一下宏的用法。宏(macro)是 rust 中的是某一组相关功能的集合名字,它包含声明宏(declarative macro)
和过程宏(procedural macro)
,别急,我们先来看一些概念性的东西,然后分别上手尝试下
声明宏是什么:
声明宏
使用macro_rules!
宏来定义,比如println!
和vec!
都可以使用macro_rules!
宏来定义
过程宏是什么:
还记得我们为结构体和枚举使用属性
#[derive(Debug)]
来为结构体实现Debug trait
吗?这就是过程宏
的能力了,这种称之为自定义derive宏
除了为结构体和枚举添加
自定义derive宏
之外,过程宏
还有一种能力,可以任意的条目(比如函数)添加自定义属性的属性宏
还有一种
函数宏
,它把编译器产出的词法token
作为参数,然后构建出新代码
宏与函数有什么差别吗?
宏是一种用于编写其他代码的代码编写方式,也就是所谓的元编程范式(metaprogramming)
函数在定义签名时必须声明自己参数的个数与类型,而宏则能够处理可变数量的参数
宏的定义要比函数定义复杂得多,宏定义通常要比函数定义更加难以阅读、理解及维护
当在某个文件中调用宏时,必须在调用前定义宏或将宏引入当前作用域中,而函数则可以在任意位置定义并在任意位置使用
声明宏 macro_rules!
声明宏
也被称作模板宏,它会将输入的值与带有相关执行代码的模式进行比较:此处的值是传递给宏的字面 rust 源代码,而此处的模式则是可以用来匹配这些源代码的结构。在编译时,当某个模式匹配成功时,该分支下的代码就会被用来替换传入宏的代码,巴拉巴拉一大堆书中的定义,下面我们先回一下vec!
宏的使用:
我们来尝试实现一个简化版vec!
的定义:
在编译阶段宏会被展开成下面的样子:
来使用一下:
声明宏
是根据模式匹配来替换代码的行为,而过程宏
主要是操作输入的 rust 代码,并生成另外一些 rust 代码作为结果。当前由于技术原因,当创建过程宏
时,宏的定义必须单独放在它们自己的包中,并标注的包类型。
过程宏之自定义 derive 宏
仔细阅读这段话哦,这次我们创建一个hello_macro
的包,并在其中定义一个拥有关联函数hello_macro
的HelloMacro trait
。提供一个能够"自动实现 trait 的过程宏
"。然后呢,开发者只需要在他们类型上标注#[derive(HelloMacro)]
,就可以得到hello_macro
函数的默认实现,调用hello_macro
函数后,会打印文本:"Hello Macro! MyName is TypeName",其中的TypeName
替换为开发者的类型的名称,就像下面这样:
首先我们定义 HelloMacro trait 以及其关联函数:
然后定义过程宏
,过程宏
需要被单独放置到它们自己的包内(未来可能会取消这个限制),实现一个自定义派生过程宏
的包,命名习惯一般是:包名称_derive
,在hello_macro
项目同一级别文件夹中创建一个名为hello_macro_derive
的包:
当前目录结构如下:
实现一个过程宏
还需要三个陌生的包,我们只需要简单了解它们的用处就好:
proc_macro
:这个包是 rust 内置,编译器用来读取和操作 rust 代码的 APIsyn
:用来解析 rust 代码产生抽象语法树ast
quote
:将syn
产生的语法树重新生成 rust 代码
然后将这三个包添加到依赖中(是不是很像前端的 babel 工具系列),注意需要使用proc-macro = true
来声明这是一个这个包是过程宏(proc-macro)
的包:
准备了半天,重头戏来了,开始编写过程宏
:
解析之后的ast
结构如下:
继续实现上面的impl_hello_macro
函数:
最后还需要在hello_macro
包中引用hello_macro_derive
:
完成之后在hello_macro
项目中运行cargo run
,便可以看到控制台输出:Hello Macro! MyName is Pancakes
过程宏之属性宏
与自定义derive宏
类似,属性宏
允许创建新的属性,而不是为 derive 属性生成代码,自定义derive宏
只能被用于结构体和枚举,而属性则可以同时被用于其他条目,比如函数等,比如我们编写 Web 应用框架接口时为函数添加接口方法和路径:
使用#[proc_macro_attribute]
属性将一个函数定义为一个过程宏
。其宏定义的函数签名看起来像这样:
拿上边编写 Web 应用框架接口的例子来说,route
参数中,attr
指代属性部分:#[route(GET, "/")]
,item
指代函数体:fn index() {}
,总体来说还是和自定义derive宏
使用方式很相似的,介于篇幅原因就不多介绍了,大家可以先把基础知识打扎实,有精力的话再去深入研究
过程宏之函数宏
函数宏
可以定义出类似于函数调用的宏,与macro_rules!
宏类似,函数宏
也能接收未知数量的参数
函数宏与 macro_rules 的区别是什么?
macro_rules!
宏只能使用类似于match
的语法来进行定义,而函数宏
则可以接收一个TokenStream
作为参数,与自定义derive宏
和属性宏
一样,可以在定义中使用 rust 代码来操作TokenStream
,例如下面这个sql!
宏:
sql!
宏的签名是这样的:
函数宏
与自定义derive宏
的签名类似,接收TokenStream
作为参数,然后返回生成的代码,所以你发现了,声明宏
就是用类似正则匹配的方式,而三种过程宏
都是对 rust 代码的编辑再生成,所以声明宏
肯定是没有过程宏
灵活的,过程宏
也没有声明宏
编写起来高效,本篇只是简单介绍了宏的能力,如果想更加深入,可以去阅读宏小册
最后
本篇是 rust 入门系列的最后一篇,希望大家对 rust 有了基本的了解,后面可能会有一些小 demo 的文章供大家练手,本人也在学习过成功,所以希望可以和大家多多交流,共同进步吧^_^。
封面图:by 铁柱呆又呆
关注「码生笔谈」公众号,阅读更多有趣文章
版权声明: 本文为 InfoQ 作者【码生笔谈】的原创文章。
原文链接:【http://xie.infoq.cn/article/a144762979b4af964359965b4】。文章转载请联系作者。
评论