【译】Rust宏:教程与示例(一)
原文标题:Macros in Rust: A tutorial with examples
原文链接:https://blog.logrocket.com/macros-in-rust-a-tutorial-with-examples/
公众号: Rust 碎碎念
翻译 by: Praying
在本文中,我们将会涵盖你需要了解的关于 Rust 宏(macro)的一切,包括对 Rust 宏的介绍和如何使用 Rust 宏的示例。
我们会涵盖以下内容:
Rust 宏是什么?
Rust 宏的类型
Rust 宏的声明
创建声明式宏 Rust 中声明式宏的高级解析 从结构体中解析元数据 声明式宏的限制
Rust 中的过程宏
属性式风格宏 自定义继承宏 函数式风格宏
Rust 宏是什么?
Rust 对宏(macro)有着非常好的支持。宏能够使得你能够通过写代码的方式来生成代码,这通常被称为元编程(metaprogramming)。
宏提供了类似函数的功能,但是没有运行时开销。但是,因为宏会在编译期进行展开(expand),所以它会有一些编译期的开销。
Rust 宏非常不同于 C 里面的宏。Rust 宏会被应用于词法树(token tree),而 C 语言里的宏则是文本替换。
Rust 宏的类型
Rust 有两种类型的宏:
声明式宏(Declarative macros)使得你能够写出类似 match 表达式的东西,来操作你所提供的 Rust 代码。它使用你提供的代码来生成用于替换宏调用的代码。
过程宏(Procedural macros)允许你操作给定 Rust 代码的抽象语法树(abstract syntax tree, AST)。过程宏是从一个(或者两个)
TokenStream
到另一个TokenStream
的函数,用输出的结果来替换宏调用。
让我们来看一下声明式宏和过程宏的更多细节,并讨论一些关于如何在 Rust 中使用宏的例子。
Rust 中的声明式宏
宏通过使用macro_rules!
来声明。声明式宏虽然功能上相对较弱,但提供了易于使用的接口来创建宏来移除重复性代码。最为常见的一个声明式宏就是println!
。声明式宏提供了一个类似match
的接口,在匹配时,宏会被匹配分支的代码替换。
创建声明式宏
macro_rules! add{
// macth like arm for macro
($a:expr,$b:expr)=>{
// macro expand to this code
{
// $a and $b will be templated using the value/variable provided to macro
$a+$b
}
}
}
fn main(){
// call to macro, $a=1 and $b=2
add!(1,2);
}
这段代码创建了一个宏来对两个数进行相加。[macro_rules!]与宏的名称,add
,以及宏的主体一同使用。
这个宏没有对两个数执行相加操作,它只是把自己替换为把两个数相加的代码。宏的每个分支接收一个函数的参数,并且参数可以被指定多个类型。如果想要add
函数也能仅接收一个参数,我们可以添加另一个分支:
macro_rules! add{
// first arm match add!(1,2), add!(2,3) etc
($a:expr,$b:expr)=>{
{
$a+$b
}
};
// Second arm macth add!(1), add!(2) etc
($a:expr)=>{
{
$a
}
}
}
fn main(){
// call the macro
let x=0;
add!(1,2);
add!(x);
}
在一个宏中,可以有多个分支,宏根据不同的参数展开到不同的代码。每个分支可以接收多个参数,这些参数使用$
符号开头,然后跟着一个 token 类型:
item
——一个项(item),像一个函数,结构体,模块等。block
——一个块 (block)(即一个语句块或一个表达式,由花括号所包围)stmt
—— 一个语句(statement)pat
——一个模式(pattern)expr
—— 一个表达式(expression)ty
——一个类型(type)ident
—— 一个标识符(indentfier)path
—— 一个路径(path)(例如,foo
,::std::mem::replace
,transmute::<_, int>
,...)meta
—— 一个元数据项;位于#[...]
和#![...]
属性tt
——一个词法树vis
——一个可能为空的Visibility
限定词
在上面的例子中,我们使用$typ
参数,它的 token 类型为ty
,类似于u8
,u16
。这个宏在对数字进行相加之前转换为一个特定的类型。
macro_rules! add_as{
// using a ty token type for macthing datatypes passed to maccro
($a:expr,$b:expr,$typ:ty)=>{
$a as $typ + $b as $typ
}
}
fn main(){
println!("{}",add_as!(0,2,u8));
}
Rust 宏还支持接收可变数量的参数。这个操作非常类似于正则表达式。*
被用于零个或更多的 token 类型,+
被用于零个或者一个参数。
macro_rules! add_as{
(
// repeated block
$($a:expr)
// seperator
,
// zero or more
*
)=>{
{
// to handle the case without any arguments
0
// block to be repeated
$(+$a)*
}
}
}
fn main(){
println!("{}",add_as!(1,2,3,4)); // => println!("{}",{0+1+2+3+4})
}
重复的 token 类型被$()
包裹,后面跟着一个分隔符和一个*
或一个+
,表示这个 token 将会重复的次数。分隔符用于多个 token 之间互相区分。$()
后面跟着*
和+
用于表示重复的代码块。在上面的例子中,+$a
是一段重复的代码。
如果你更仔细地观察,你会发现这段代码有一个额外的 0 使得语法有效。为了移除这个 0,让add
表达式像参数一样,我们需要创建一个新的宏,被称为TT muncher。
macro_rules! add{
// first arm in case of single argument and last remaining variable/number
($a:expr)=>{
$a
};
// second arm in case of two arument are passed and stop recursion in case of odd number ofarguments
($a:expr,$b:expr)=>{
{
$a+$b
}
};
// add the number and the result of remaining arguments
($a:expr,$($b:tt)*)=>{
{
$a+add!($($b)*)
}
}
}
fn main(){
println!("{}",add!(1,2,3,4));
}
TT muncher 以递归方式分别处理每个 token,每次处理单个 token 也更为简单。这个宏有三个分支:
第一个分支处理是否单个参数通过的情况
第二个分支处理是否两个参数通过的情况
第三个分支使用剩下的参数再次调用
add
宏
宏参数不需要用逗号分隔。多个 token 可以被用于不同的 token 类型。例如,圆括号可以结合ident
token 类型使用。Rust 编译器能够匹配对应的分支并且从参数字符串中导出变量。
macro_rules! ok_or_return{
// match something(q,r,t,6,7,8) etc
// compiler extracts function name and arguments. It injects the values in respective varibles.
($a:ident($($b:tt)*))=>{
{
match $a($($b)*) {
Ok(value)=>value,
Err(err)=>{
return Err(err);
}
}
}
};
}
fn some_work(i:i64,j:i64)->Result<(i64,i64),String>{
if i+j>2 {
Ok((i,j))
} else {
Err("error".to_owned())
}
}
fn main()->Result<(),String>{
ok_or_return!(some_work(1,4));
ok_or_return!(some_work(1,0));
Ok(())
}
ok_or_return
这个宏实现了这样一个功能,如果它接收的函数操作返回Err
,它也返回Err
,或者如果操作返回Ok
,就返回Ok
里的值。它接收一个函数作为参数,并在一个 match 语句中执行该函数。对于传递给参数的函数,它会重复使用。
通常来讲,很少有宏会被组合到一个宏中。在这些少数情况中,内部的宏规则会被使用。它有助于操作这些宏输入并且写出整洁的 TT munchers。
要创建一个内部规则,需要添加以@
开头的规则名作为参数。这个宏将不会匹配到一个内部的规则除非显式地被指定作为一个参数。
macro_rules! ok_or_return{
// internal rule.
(@error $a:ident,$($b:tt)* )=>{
{
match $a($($b)*) {
Ok(value)=>value,
Err(err)=>{
return Err(err);
}
}
}
};
// public rule can be called by the user.
($a:ident($($b:tt)*))=>{
ok_or_return!(@error $a,$($b)*)
};
}
fn some_work(i:i64,j:i64)->Result<(i64,i64),String>{
if i+j>2 {
Ok((i,j))
} else {
Err("error".to_owned())
}
}
fn main()->Result<(),String>{
// instead of round bracket curly brackets can also be used
ok_or_return!{some_work(1,4)};
ok_or_return!(some_work(1,0));
Ok(())
}
在 Rust 中使用声明式宏进行高级解析
宏有时候会执行需要解析 Rust 语言本身的任务。
让我们创建一个宏把我们到目前为止讲过的所有概念融合起来,通过pub
关键字使其成为公开的。
首先,我们需要解析 Rust 结构体来获取结构体的名字,结构体的字段以及字段类型。
解析结构体的名字及其字段
一个struct
(即结构体)声明在其开头有一个可见性关键字(比如pub
) ,后面跟着struct
关键字,然后是struct
的名字和struct
的主体。
macro_rules! make_public{
(
// use vis type for visibility keyword and ident for struct name
$vis:vis struct $struct_name:ident { }
) => {
{
pub struct $struct_name{ }
}
}
}
$vis
将会拥有可见性,$struct_name
将会拥有一个结构体名。为了让一个结构体是公开的,我们只需要添加pub
关键字并忽略$vis
变量。
一个struct
可能包含多个字段,这些字段具有相同或不同的数据类型和可见性。ty
token 类型用于数据类型,vis
用于可见性,ident
用于字段名。我们将会使用*
用于零个或更多字段。
macro_rules! make_public{
(
$vis:vis struct $struct_name:ident {
$(
// vis for field visibility, ident for field name and ty for field data type
$field_vis:vis $field_name:ident : $field_type:ty
),*
}
) => {
{
pub struct $struct_name{
$(
pub $field_name : $field_type,
)*
}
}
}
}
从struct
中解析元数据
通常,struct
有一些附加的元数据或者过程宏,比如#[derive(Debug)]
。这个元数据需要保持完整。解析这类元数据是通过使用meta
类型来完成的。
macro_rules! make_public{
(
// meta data about struct
$(#[$meta:meta])*
$vis:vis struct $struct_name:ident {
$(
// meta data about field
$(#[$field_meta:meta])*
$field_vis:vis $field_name:ident : $field_type:ty
),*$(,)+
}
) => {
{
$(#[$meta])*
pub struct $struct_name{
$(
$(#[$field_meta:meta])*
pub $field_name : $field_type,
)*
}
}
}
}
我们的make_public
宏现在准备就绪了。为了看一下make_public
是如何工作的,让我们使用Rust Playground来把宏展开为真实编译的代码。
macro_rules! make_public{
(
$(#[$meta:meta])*
$vis:vis struct $struct_name:ident {
$(
$(#[$field_meta:meta])*
$field_vis:vis $field_name:ident : $field_type:ty
),*$(,)+
}
) => {
$(#[$meta])*
pub struct $struct_name{
$(
$(#[$field_meta:meta])*
pub $field_name : $field_type,
)*
}
}
}
fn main(){
make_public!{
#[derive(Debug)]
struct Name{
n:i64,
t:i64,
g:i64,
}
}
}
展开后的代码看起来像下面这样:
// some imports
macro_rules! make_public {
($ (#[$ meta : meta]) * $ vis : vis struct $ struct_name : ident
{
$
($ (#[$ field_meta : meta]) * $ field_vis : vis $ field_name : ident
: $ field_type : ty), * $ (,) +
}) =>
{
$ (#[$ meta]) * pub struct $ struct_name
{
$
($ (#[$ field_meta : meta]) * pub $ field_name : $
field_type,) *
}
}
}
fn main() {
pub struct name {
pub n: i64,
pub t: i64,
pub g: i64,
}
}
声明式宏的限制
声明式宏有一些限制。有些是与 Rust 宏本身有关,有些则是声明式宏所特有的:
缺少对宏的自动完成和展开的支持
声明式宏调式困难
修改能力有限
更大的二进制
更长的编译时间(这一条对于声明式宏和过程宏都存在)
【译】Rust宏:教程与示例(一)的更多相关文章
- 【译】Rust宏:教程与示例(二)
原文标题:Macros in Rust: A tutorial with examples 原文链接:https://blog.logrocket.com/macros-in-rust-a-tutor ...
- 【转】C#正则表达式教程和示例
[转]C#正则表达式教程和示例 有一段时间,正则表达式学习很火热很潮流,当时在CSDN一天就能看到好几个正则表达式的帖子,那段时间借助论坛以及Wrox Press出版的<C#字符串和正则表达式参 ...
- ReadyAPI 教程和示例(一)
原文:ReadyAPI 教程和示例(一) 声明:如果你想转载,请标明本篇博客的链接,请多多尊重原创,谢谢! 本篇使用的 ReadyAPI版本是2.5.0 通过下图你可以快速浏览一下主要的ReadyAP ...
- Terraform入门教程,示例展示管理Docker和Kubernetes资源
我最新最全的文章都在南瓜慢说 www.pkslow.com,欢迎大家来喝茶! 1 简介 最近工作中用到了Terraform,权当学习记录一下,希望能帮助到其它人. Terraform系列文章如下: T ...
- [译]MVC网站教程(四):MVC4网站中集成jqGrid表格插件(系列完结)
目录 1. 介绍 2. 软件环境 3. 在运行示例代码之前(源代码 + 示例登陆帐号) 4. jqGrid和AJAX 5. GridSettings 6. ...
- [译]MVC网站教程(三):动态布局和站点管理
目录 1. 介绍 2. 软件环境 3. 在运行示例代码之前(源代码 + 示例登陆帐号) 4. 自定义操作结果和控制器扩展 1) OpenFileResult 2) ImageR ...
- [译]MVC网站教程(二):异常管理
介绍 “MVC网站教程”系列的目的是教你如何使用 ASP.NET MVC 创建一个基本的.可扩展的网站. 1) MVC网站教程(一):多语言网站框架 2) MVC网站教程(二):异常管理 3) ...
- [译]MVC网站教程(一):多语言网站框架
本文简介 本博文介绍了 Visual Studio 工具生成的 ASP.NET MVC3 站点的基本框架:怎样实现网站的语言的国际化与本地化功能,从零开始实现用户身份认证机制,从零开始实现用户注册机制 ...
- 微信小程序 教程及示例
作者:初雪链接:https://www.zhihu.com/question/50907897/answer/128494332来源:知乎著作权归作者所有,转载请联系作者获得授权.微信小程序正式公测, ...
随机推荐
- 2019 Multi-University Training Contest 7 Kejin Player(期望)
题意:给定在当前等级升级所需要的花费 每次升级可能会失败并且掉级 然后q次询问从l到r级花费的期望 思路:对于单次升级的期望 我们可以列出方程: 所以我们可以统计一下前缀和 每次询问O1回答 #inc ...
- Codeforces Round #654 (Div. 2)
比赛链接:https://codeforces.com/contest/1371 A. Magical Sticks 题意 有 $n$ 根小棍,长度从 $1$ 到 $n$,每次可以将两根小棍连接起来, ...
- c语言实现--带头结点单链表操作
可能是顺序表研究的细致了一点,单链表操作一下子就实现了.这里先实现带头结点的单链表操作. 大概有以下知识点. 1;结点:结点就是单链表中研究的数据元素,结点中存储数据的部分称为数据域,存储直接后继地址 ...
- codeforces622E Ants in Leaves (dfs)
Description Tree is a connected graph without cycles. A leaf of a tree is any vertex connected with ...
- UWP(二)调用Win32程序
目录 一.如何构建Win32程序 二.如何构建UWP工程? 三.Samples 一.如何构建Win32程序 打开csproj文件,使用如下代码添加引用(Reference).注意,如果指定位置不存在, ...
- Spring:解决因@Async引起的循环依赖报错
最近项目中使用@Async注解在方法上引起了循环依赖报错: org.springframework.beans.factory.BeanCurrentlyInCreationException: Er ...
- POJ 1625 Censored!(AC自动机 + DP + 大数 + 拓展ASCII处理)题解
题意:给出n个字符,p个病毒串,要你求出长度为m的不包含病毒串的主串的个数 思路:不给取模最恶劣情况$50^{50}$,所以用高精度板子.因为m比较小,可以直接用DP写. 因为给你的串的字符包含拓展A ...
- 问题记录 java.lang.NoClassDefFoundError: org/dom4j/DocumentException
客户端调webservice服务产生以下错误 AxisFault faultCode: {http://schemas.xmlsoap.org/soap/envelope/}Server.genera ...
- vue watch route params change
vue watch route params change watch: { '$route.params.menuKey' (val, oldVal) { console.log('new rout ...
- pagehide event & sendBeacon
pagehide event & sendBeacon 通过 API 测试 pagehide 是否触发了 pagehide 不支持正常的 fetch 请求发送 pagehide 仅支持 sen ...