19 高级特征

我们将在这一章学习更多高级功能

19.5 宏

宏指的是Rust中一系列功能,宏用macro_rules!来声明

三种过程宏:

1.自定义#[derive]宏在结构体和枚举上指定通过derive属性添加的代码

2.类属性宏定义可用于任意项的自定义属性

3.类函数宏看起来像函数不过作用于作为参数传递的token

宏和函数的区别

从根本上来说,宏是一种为写其它代码而写代码的方式,即所谓元编程

所有宏以展开的方式来生成比我们手写出的更多的代码

元编程对于减少代码量和维护代码非常有用,它也扮演了函数的角色,但是宏有一些函数所没有的能力

1.函数声明时必须声明参数的个数和类型,但是宏不限参数数量,如:

println!("hello");
println!("hello {}",name)

2.宏可以在编译器翻译代码前展开,例如宏可以在给定的类型是实现trait。但是函数不行,因为函数是在运行时被调用,同时trait需要在编译时实现

3.实现宏更加复杂,因为它的定义复杂,你是在编写生成Rust代码的Rust代码。由于这样的间接性,宏的定义通常要比函数定义更加难阅读、理解以及维护

4.在一个文件里调用宏之前必须定义它,或将其引入作用域,而函数则可以在任何地方定义和调用

使用macro_rules!的声明宏用于通用元编程

Rust最常用的宏形式是声明宏,有时也被称为 “macros by example”、“macro_rules!宏”、或者就是“macros”。其核心概念就是声明宏允许我们编写一些类似Rust match表达式的代码

match表达式是控制结构,宏也是将一个值和包含相关代码的模式进行比较;这种情况下,该值是传递给宏的Rust源代码字面值,模式用于与其相比较,每个模式对应的代码用来替换传递给宏的代码,所有的这一切都发生在编译时

可以使用macro_rules!来定义宏。让我们查看vec!宏定义来探索macro_rules!结构

let v :Vec<u32>= vec![1,2,3];

使用vec!宏创建了一个由三个整数组成的vector,但是函数却无法做这样的事情,因为我们预先无法知道参数的数量和类型

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

一个vec!宏定义的简化版本,在标准库中,实际定义的vec!包括预分配适量的内存的代码。这部分为代码的优化,我们这里为了简化,所以并未包括在内

无论何时导入定义了宏的包,#[macro_export]注解说明宏时可用的。如果没有这个注解,这个宏就不能被引入作用域

使用macro_rules!后接宏名称定义宏,大括号中写定义体

vec!宏的结构和match结构类似,此处有一个单边模式((((x:expr),*),后跟=>以及和模式相关的代码块。·如果模式匹配,该相关代码块将被执行。假设这是这个宏唯一的模式,则只有这一种有效的匹配,其他任何匹配都是错误的,更复杂的宏会有多个单边模式

另外宏模式所匹配的是Rust代码结构而不是而不是值

首先,一对括号包含了整个模式。接着的后根一堆括号,捕获了符合括号内模式的值以用于替换后的代码。后根一堆括号,捕获了符合括号内模式的值以用于替换后的代码。后根一堆括号,捕获了符合括号内模式的值以用于替换后的代码。()内则是x:expr,其匹配Rust的任意表达式,并将该表达式记作x:expr,其匹配Rust的任意表达式,并将该表达式记作x:expr,其匹配Rust的任意表达式,并将该表达式记作x

()之后的逗号说明一个可有可无的逗号分隔符可以出现在()之后的逗号说明一个可有可无的逗号分隔符可以出现在()之后的逗号说明一个可有可无的逗号分隔符可以出现在()所匹配的代码之后。紧随逗号之后的说明该模式匹配零个或多个之前的任何模式

当以vec![1,2,3]调用宏时,$x模式与三个表达式1,2,3进行了三次匹配

我们来看一下与此单边模式相关联的代码块中的模式:对于每个匹配模式中的()的部分,生成零个或更多个位于()的部分,生成零个或更多个位于()的部分,生成零个或更多个位于x()*内的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

这个宏就是可以接收任意数量和类型的参数,同时可以生成能够创建包含指定元素的vector的代码

macro_rules!中有一些奇怪的地方,以后我们会使用macro关键字的声明宏,其工作方式类似但是修复了这些计算情况。macro_rules!实际上在此之后就过时了。另外,其实程序员们大多在实际中是使用宏而不是编写宏,所以就不多展开了,更多内容可以查看文档

用于从属性生成代码的过程宏

第二种形式的宏被称为过程宏,因为它们更像函数(一种过程类型)。过程宏接收Rust代码作为输入,然后在此之上操作,产生另一些代码作为输出

过程宏有三种类型:自定义派生derive、类属性和类函数,它们的工作方式类似

注意:创建过程宏时,其定义必须驻留在它们的具有特殊crate类型的crate中。这是因为复杂的技术原因

使用过程宏的代码形式

use proc_macro;#[some_attribute]
pub fn some_name(input:TokenStream) -> TokenStream {
}

TokenStream类型由包含在Rust中的pro_macro crate 定义并表示令牌序列。这是宏的核心:宏所操作的源代码构成了输入TokenStream,宏产生的代码是输出TokenStream。该函数还附加了一个属性,用于指定我们正在创建的程序宏类型。我们可以在同一个crate中拥有多种程序宏

如何编写自定义derive 宏

让我们创建一个hello_macro crate,其包含名为 HelloMacro 的trait和关联函数hello_macro

不同于让crate用户为每个类型实现 HelloMacro trait,我们来提供一个过程式宏 以便用户可以使用 #[derive(HelloMacro)]注解他们的类型来得到hello_macro 函数的默认实现。该默认实现会打印 Hello,Macro!My name is TypeName!,其中TypeName为定义了trait的类型名。换言之,我们会创建一个crate,使得能够编写如下类型的代码

use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;#[derive(HelloMacro)]
struct Pancakes;
fn main(){Pancakes::hello_macro();
}

crate 用户所写的能够使用过程式宏的代码

接下来我们就来一步一步实现这个目标

我们先建一个库crate

cargo new hello_macro --lib

定义HelloMacro trait以及其关联函数

pub trait  HelloMacro {fn hello_macro();
}

现在有了一个包含函数的trait,此时,crate用户可以实现该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();
}
  Running `target/debug/hello_macro`
Hello, Macro! My name is Pancakes!

但是!这种方式需要我们为每一个想使用hello_macro函数的具体类型编写实现的代码块,我们希望避免这些冗杂的工作

另外,Rust没有反射的能力,所以我们无法随着具体的类型的不同而打印机类型名字,除非一个一个改println!中的代码

现在我们定义过程宏来实现这一点,注意,过程宏必须在其自己的crate内。我们先来在hello_macro项目中新建名为hello_macro_derive的包

cargo new hello_macro_derive --lib

由于两个crate紧密相关,因此在hello_macro 包的目录下创建过程式宏的crate

如果改变在hello_macro中定义的trait,同时也必须改变在hello_macro_derive中实现的过程式宏。这两个包需要分别发布,编程人员如果使用这些包,则需要同时添加这两个依赖并将其引入作用域

我们也可以只用hello_macro包而将hello_macro_derive 作为一个依赖,并重新导出过程式宏的代码

但现在我们组织项目的方式使编程人员在无需derive 功能时也能够单独使用hello_macro

我们需要声明hello_macro_derive crate 是过程宏(proc-macro)crate.正如稍后看到的那样,我们还需要syn和quote crate中的功能,所以需要将其添加到依赖中

hello_macro_derive/Cargo.toml

[lib]pro-macro = true[dependencies]
syn = "1.0"
quote = "1.0"

为定义一个过程式宏,还需要在hello_macro_derive crate 的src/lib.rs中添加如下代码

extern crate proc_macro;use crate::proc_macro::TokenStream;
use quote::quote;
sue syn;#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input:TokenStream)->TokenStream {//将Rust代码解析为语法树以便进行操作let ast = syn::parse(input).unwrap();//构建trait 实现impl_hello_macro(&ast)
}

大多数过程式宏处理Rust代码时所需的代码

注意;这段代码在我们添加impl_hello_macro函数的定义之前是无法编译的

另外再注意hello_macro_derive函数中代码分割方式,他负责解析TokenStream,而impl_hello_macro函数则负责转换语法树:这让编写一个过程式宏更加方便

外部函数中的代码(这里是hello_macro_derive)几乎在所有你能看到或创建的过程宏crate中都一样。内部函数(在这里是impl_hello_macro)的函数体中所指定的代码则以过程宏的目的而各有不同

现在我们已引入了三个crate:pro-macro,syn,quote,Rust自带pro-macro crate,因此无需将其加入到Cargo.toml文件的依赖中。proc_macro crate是编译器用来读取和操作我们Rust代码的API

syn crate 将字符串中的Rust代码解析为一个可操作的数据结构

quote crate 则将syn 解析的数据结构转换为Rust代码,这些crate让解析任何我们要处理的Rust代码变得简单:为Rust编写整个解析器并不是一件简单的工作

当用户在一个类型上指定#[derive(HelloMacro)]时,hello_macro_derive函数将会被调用,原因在于我们已经使用proc_macro_derive及其指定名称对hello_macro_derive函数进行了注解

HelloMacro,其匹配到trait名,这是大多数过程宏遵循的习惯

该函数首先将来自TokenStream的input转换为一个我们可以解释和操作的数据结构。这正是syn派上用场的地方。syn中的parse_derive_input函数获取一个TokenStream并返回一个表示解析出Rust代码的DeriveInput结构体

如下展示了从字符串struct Pancakes中解析出来的DeriveInput 结构体的相关部分

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

解析带有宏属性的代码时得到的DeriveInput 实例

该结构展示了我们解析得Rust代码是一个类单元结构体,其ident为Pancakes。该结构体里面有更多字段描述了所有类型得Rust代码,可以查阅文档获取更多信息

现在我们还没有定义impl_hello_macro函数,其用于构建所要包含在内得Rust新代码。但在此之前,注意其输出也是TokenStream。所返回的TokenStream会被加到我们的crate用户所写的代码中,因此,当用户编译他们的crate时,他们会获取到我们所提供的额外功能

当调用syn::parse 函数失败时,我们用unwrap来使hello_macro_derive函数panic,在错误时oanic对于过程宏来说是必须的,因为pro_macro_dervie函数必须返回TokenStream而不是Result,以此来符合过程宏的API

这里其实是选择了unwrap来做了简化,在成产代码中,则应该是通过panic!或者expect来提供关于发生何种错误的更加明确的错误信息

现在我们有了将注解的Rust代码从TokenStream转换为DeriveInput实例的代码,让我们来创建在注解类型上实现HelloMacro trait的代码

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

使用解析过的Rust代码实现HelloMacro trait

我们得到一个包含ast.ident作为注解类型名字(标识符)的Ident 结构体的实例

quote!宏让我们可以编写希望返回的Rust代码。Quote!宏执行的直接结果并不是编译器所期望的需要转换为TokenStream,为此需要调用into方法,它会消费这个中间表示并返回所需要的TokenStream类型值

这个宏也提供了一些非常酷的模板机制:我们可以写#name,然后quote!会以名为name的变量值来替换他。甚至可以做一些类似常用宏那样的重复代码的工作,具体可以查看crate文档

我们希望过程宏能够为通过#name 获取到的用户注解类型生成HelloMacro trait 的实现。该trait实现有一个函数 hello_macro,其函数整体包括了我们期望提供的功能:打印Hello,Macro!My name is 和注解的类型名

此处所使用的stringify!为Rust内置宏。其接收一个Rust表达式,如 1+2 然后在编译时将表达式转换为一个字符串常量,如“1+2”

这与format!或 println!是不同的,它计算表达式并将结果转为string。有一种情况是,所输入的#name可能是一个需要打印的表达式,因此我们用stringfy!。stringfy!编译时也保留了一份将#name转换为字符串之后的内存分配

此时,cargo build应该都能编译hello_macro 和 hello_macro_derive。我们将这些crate连接到使用过程宏的代码,并且看看过程宏的行为。在projects目录下用cargo new pancakes 命令新建一个二进制项目,并且我们把hello_macro 和 hello_macro_derive作为依赖加到pancakes包的Cargo.toml文件中

如果你正将hello_macro 和 hello_macro_derive的版本发布到crates.io上,其应为常规依赖,如果不是,则可以像下面这样将其指定为path依赖:

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

现在运行代码,就可以打印出我们想要的内容。其包含了该过程宏HelloMacro trait 的实现,而无pancakes crate 实现它.#[derive(HelloMacro)]增加了该trait实现

下面,我们看看其它类型的过程宏与自定义派生宏有何区别

类属性宏

类属性宏与自定义派生宏相似,不同于derive属性生成代码,他们允许你创建新的属性。它们也更为灵活,derive只能用于结构体和枚举,属性还可以用于其他的项,比如函数。作为一个使用类属性宏的例子,可以创建一个名为route的属性用于注解web应用程序框架(web application framework)的函数

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

#[route] 属性将由框架本身定义为一个过程宏,其宏定义的函数签名看起来像这样:

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

这里有两个TokenStream类型的参数,第一个用于属性内容本身,也就是GET,“/”部分。第二个是属性所标记的项:在本例中,是fn index() {} 和剩下的函数体

除此之外,类属性宏与自定义派生宏工作方式一致:创建proc-macro 类型的crate 并实现希望生成代码的函数!

类函数宏

类函数宏定义看起来像函数调用的宏。类似于macro_rules!它们比函数更加灵活;例如,可以接收位置数量的参数,然而macro_rules! 宏只能使用之前“使用macro_rules!”的声明宏用于通用元编程“
介绍的类匹配的语法定义。类函数宏获取TokenStream参数,其定义使用Rust代码操纵TokenStream,就像另两种过程宏一样,一个类似函数宏的例子是可以像这样被调用的sql!宏:

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

这个宏会解析其中的SQL语句并检查其是否是正确的,这是比macro_rules!可以做到更为复杂的处理。sql! 宏应该被定义如此:

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

这类似于自定义派生宏的签名:获取括号中的Token,并返回希望生成的代码

总结:我们现在学习了Rust并不常用但是在特定的情况下可能用的着的功能,上述是一些复杂的概念,以后遇到即使一下子不太明白,但是起码见过它们,了解解决它们的思路

下一章,我们会看一个实际的例子

The Rust Programming Language - 第19章 高级特征 - 19.5 宏相关推荐

  1. The Rust Programming Language - 第11章 测试 - 11.1 编写测试

    11 测试 编写自动化测试 程序的正确性代码如我们期望的那样运行,Rust也在语言本身包含了编写软件测试的支持 本章我们会讲到编写测试时用到的注解和宏,运行测试的默认行为和选项,以及如何将测试组织成单 ...

  2. The Rust Programming Language - 第13章 Rust语言中的函数式语言功能:迭代器与闭包 - 13.1 可以捕获其环境的匿名函数

    13 Rust语言中的函数式语言功能:迭代器与闭包 函数式编程风格通常包括将函数作为另一个函数的参数.返回值,将函数作为值赋值给变量,以供后续执行 本章中我们将会介绍以下内容: 闭包:一个可以存储在变 ...

  3. The Rust Programming Language - 第18章 模式与模式匹配 - 18.2 Refutability(可反驳性):模式是否会匹配失效

    18 模式与模式匹配 模式是Rust中的特殊语法,用来匹配类型中的结构,无论类型复杂与否.模式由以下一些内容组合而成: 字面值\解构的数组.枚举.元组或者结构体\变量\通配符\占位符,这些部分描述了我 ...

  4. The Rust Programming Language - 前言

    前言 Rust程序设计语言本质在于赋能 Rust语言会涉及"系统层面"的工作,设计内存管理.数据表示和并发等底层细节(其实就是一些计算机系统.组成原理.数据结构.网络等方面的基础知 ...

  5. 《The C Programming Language》读书笔记 说明

    <The C Programming Language>读书笔记 说明 作为笔记而言,完全是一种自写自看的行为,本来是没有必要写这篇东西的.但是作为一个生活在网络时代的学 生来说,想学好一 ...

  6. 《The C Programming Language》答案(第一章)

    <The C Programming Language>答案(第一章) P1 #include <stdio.h> main() {printf("hello, wo ...

  7. 《The C Programming Language》(2nd Ed) Introduction 翻译

    <The C Programming Language>(2nd Ed) Introduction 翻译 说明: 1.       本人非专业翻译人员,信达雅三种境界,可以达到" ...

  8. Ada 程序设计语言(The Ada Programming Language)[第一集]

    Ada 程序设计语言(The Ada Programming Language)[第一集]- - 版权(Copyright) <Ada 程序设计语言>的版权隶属于网站 VenusIC,允许 ...

  9. Ada 程序设计语言(The Ada Programming Language)[第三集]

    Ada 程序设计语言(The Ada Programming Language)[第三集]- - 第4章 记录(Record) 4.1 概述(Overview)     记录则是由命名分量(named ...

最新文章

  1. Deno 正式发布,彻底弄明白和 node 的区别
  2. 【记录】解决uni-app 用nginx反向代理出现Invalid Host header问题
  3. 权限认证php,2016年Linux认证基础知识:php做权限管理
  4. android 8 esp8266,ESP8266 WIFI模块学习之路(8)——自写Android手机APP控制直流电机正反转...
  5. 2008 noip 传纸条
  6. 【推荐软件】wingrep
  7. rsync(六)命令中文手册
  8. LeetCode 2180. 统计各位数字之和为偶数的整数个数
  9. Exchange Server2010系列之二:部署三合一角色(CAS+HT+MBX)
  10. Storm介绍实际开发注意事项
  11. 收据找不到怎么退押金_押金收据单不见了,能退押金吗,合同上有写押金多少的 - 找法网免费法律咨询...
  12. 蚂蚁链ACCA认证试题
  13. 用纯前端表格控件SpreadJS,搭建上海泛微协同OA管理平台
  14. 企业实战——Ansible自动化运维基础知识
  15. 宏病毒的研究与实例分析01——基础篇
  16. Ubuntu 配置利用aira2进行百度网盘下载
  17. 北京三大春天赏花圣地
  18. web网关_配置手册
  19. C语言人五英尺七英寸,“5英尺7英寸”等于多少厘米
  20. Proxmox VE 7.2 使用qemu-img转换磁盘格式

热门文章

  1. 大咖说|中国循环经济协会朱黎阳:数字经济与循环经济协同赋能绿色低碳转型
  2. 2017中学生计算机竞赛预赛试题答案,2017年初中化学竞赛初赛试题
  3. 解决浏览器兼容新问题
  4. PyQt5+fitz实现图片与PDF互相转换
  5. 不会聊天怎么办?先做好这10件小事
  6. VMware Workstation 安装centOS
  7. 雅虎财经api_带有Yahoo API的Android反向地理编码– PlaceFinder
  8. 高通开发系列 - Voice Call之语音通话流程和问题分析
  9. 【CentOS 7.0】配置免费阿里云Docker镜像加速器
  10. JPEG系列二 JPEG文件中的EXIF(下)