这些人群不太适合吃冷饮 http://www.bdfyy999.com/bdf/yufangbaojian/binglibingyin/m/53310.html宏
你有没有注意到rust中的函数参数都是固定数量的,而print!宏和vec!宏的参数数量却是任意的?这一节我们就看一下宏的用法。宏(macro)是rust中的是某一组相关功能的集合名字,它包含声明宏(declarativemacro)和过程宏(proceduralmacro),别急,我们先来看一些概念性的东西,然后分别上手尝试下
声明宏是什么:声明宏使用macro_rules!宏来定义,比如println!和vec!都可以使用macro_rules!宏来定义过程宏是什么:还记得我们为结构体和枚举使用属性#[derive(Debug)]来为结构体实现Debugtrait吗?这就是过程宏的能力了,这种称之为自定义derive宏除了为结构体和枚举添加自定义derive宏之外,过程宏还有一种能力,可以任意的条目(比如函数)添加自定义属性的属性宏还有一种函数宏,它把编译器产出的词法token作为参数,然后构建出新代码宏与函数有什么差别吗?宏是一种用于编写其他代码的代码编写方式,也就是所谓的元编程范式(metaprogramming)函数在定义签名时必须声明自己参数的个数与类型,而宏则能够处理可变数量的参数宏的定义要比函数定义复杂得多,宏定义通常要比函数定义更加难以阅读、理解及维护当在某个文件中调用宏时,必须在调用前定义宏或将宏引入当前作用域中,而函数则可以在任意位置定义并在任意位置使用声明宏macro_rules!声明宏也被称作模板宏,它会将输入的值与带有相关执行代码的模式进行比较:此处的值是传递给宏的字面rust源代码,而此处的模式则是可以用来匹配这些源代码的结构。在编译时,当某个模式匹配成功时,该分支下的代码就会被用来替换传入宏的代码,巴拉巴拉一大堆书中的定义,下面我们先回一下vec!宏的使用:
letv:Vec_=vec![1,2,3];println!("{:?}",v);//[1,2,3]
我们来尝试实现一个简化版vec!的定义:
macro_rules!vec2{//()中表示模式,想想一下正则中的分组功能。//其中expr表示匹配表达式,item是将匹配的表达式进行命名。//括号后面的逗号意味着可能有多个参数,//*号表示前面的所有字符(包括()和,)出现0到多次(这里和一般的正则不同哦)((item:expr),*)={{letmuttemp_vec=Vec::new();(temp_vec.push(item);)*//匹配了多少次参数,就编译出多少次这个语句temp_vec}}}
在编译阶段宏会被展开成下面的样子:
letmuttemp_vec=Vec::new();temp_vec.push(1);temp_vec.push(2);temp_vec.push(3);temp_vec
来使用一下:
letv2=vec2![1,2,3];println!("{:?}",v2);//[1,2,3]
声明宏是根据模式匹配来替换代码的行为,而过程宏主要是操作输入的rust代码,并生成另外一些rust代码作为结果。当前由于技术原因,当创建过程宏时,宏的定义必须单独放在它们自己的包中,并标注的包类型。
过程宏之自定义derive宏仔细阅读这段话哦,这次我们创建一个hello_macro的包,并在其中定义一个拥有关联函数hello_macro的HelloMacrotrait。提供一个能够"自动实现trait的过程宏"。然后呢,开发者只需要在他们类型上标注#[derive(HelloMacro)],就可以得到hello_macro函数的默认实现,调用hello_macro函数后,会打印文本:"HelloMacro!MyNameisTypeName",其中的TypeName替换为开发者的类型的名称,就像下面这样:
//hello_macro/src/main.rsusehello_macro::HelloMacro;//引入traitusehello_macro_derive::HelloMacro;//引入自定义derive宏#[derive(HelloMacro)]//使用自定义derive宏实现HelloMacrotraitstructPancakes;Pancakes::hello_macro();//HelloMacro!MyNameisPancakes!
首先我们定义HelloMacrotrait以及其关联函数:
//hello_macro/src/lib.rspubtraitHelloMacro{fnhello_macro();}
然后定义过程宏,过程宏需要被单独放置到它们自己的包内(未来可能会取消这个限制),实现一个自定义派生过程宏的包,命名习惯一般是:包名称_derive,在hello_macro项目同一级别文件夹中创建一个名为hello_macro_derive的包:
cargonewhello_macro_derive--lib
当前目录结构如下:
├──hello_macro│├──Cargo.toml│└──src│├──lib.rs│└──main.rs└──hello_macro_derive├──Cargo.toml└──src└──lib.rs
实现一个过程宏还需要三个陌生的包,我们只需要简单了解它们的用处就好:
proc_macro:这个包是rust内置,编译器用来读取和操作rust代码的APIsyn:用来解析rust代码产生抽象语法树astquote:将syn产生的语法树重新生成rust代码然后将这三个包添加到依赖中(是不是很像前端的babel工具系列),注意需要使用proc-macro=true来声明这是一个这个包是过程宏(proc-macro)的包:
//hello_macro_derive/Cargo.toml[lib]proc-macro=true[dependencies]syn="1.0"quote="1.0"
准备了半天,重头戏来了,开始编写过程宏:
//hello_macro_derive/src/lib.rsuseproc_macro::TokenStream;//词法token类型usequote::quote;usesyn;//当开发者在一个类型上指定#[derive(HelloMacro)]时,//hello_macro_derive函数将会被调用#[proc_macro_derive(HelloMacro)]pubfnhello_macro_derive(input:TokenStream)-TokenStream{//将rust语法转为抽象语法树letast=syn::parse(input).unwrap();//实现hello_macroimpl_hello_macro(ast)}
解析之后的ast结构如下:
DeriveInput{//--略--ident:Ident{ident:"Pancakes",//我们需要获取这个字段span:#0bytes(95..)},data:Struct(DataStruct{struct_token:Struct,fields:Unit,semi_token:Some(Semi)})}
继续实现上面的impl_hello_macro函数:
fnimpl_hello_macro(ast:syn::DeriveInput)-TokenStream{//获取"Pancakes"letname=ast.ident;//quote!宏可以用来编写rust代码,这里生成一段为Pancakes实现HelloMacrotrait的代码letgen=quote!{//这里使用了模板机制,#name用来引用"Pancakes"implHelloMacrofor#name{fnhello_macro(){//由于要打印出类型的名称,所以这里也需要用#name来替换为"Pancakes"//这里使用的stringify!宏是内置在rust中的,它接收一个rust表达式,在编译时将这个表达式转换成字符串字面量,如下://println!("{}",stringify!(1+2));//1+2println!("HelloMacro!MyNameis{}",stringify!(#name))//代码中输入的#name有可能是一个表达式,因为我们希望直接打印出这个值的字面量,所以这里使用了stringify!}}};//quote!宏执行的直接结果并不是编译器所期望的并需要转换为TokenStream。//为此需要调用into方法,它会消费这个中间表示(intermediaterepresentation,IR)并返回所需的TokenStream类型值。gen.into()}
最后还需要在hello_macro包中引用hello_macro_derive:
//hello_macro/Cargo.toml[dependencies]hello_macro_derive={path="../hello_macro_derive"}
完成之后在hello_macro项目中运行cargorun,便可以看到控制台输出:HelloMacro!MyNameisPancakes
过程宏之属性宏与自定义derive宏类似,属性宏允许创建新的属性,而不是为derive属性生成代码,自定义derive宏只能被用于结构体和枚举,而属性则可以同时被用于其他条目,比如函数等,比如我们编写Web应用框架接口时为函数添加接口方法和路径:
#[route(GET,"/")]fnindex(){}
使用#[proc_macro_attribute]属性将一个函数定义为一个过程宏。其宏定义的函数签名看起来像这样:
#[proc_macro_attribute]pubfnroute(attr:TokenStream,item:TokenStream)-TokenStream{}
拿上边编写Web应用框架接口的例子来说,route参数中,attr指代属性部分:#[route(GET,"/")],item指代函数体:fnindex(){},总体来说还是和自定义derive宏使用方式很相似的,介于篇幅原因就不多介绍了,大家可以先把基础知识打扎实,有精力的话再去深入研究
过程宏之函数宏函数宏可以定义出类似于函数调用的宏,与macro_rules!宏类似,函数宏也能接收未知数量的参数
函数宏与macro_rules的区别是什么?macro_rules!宏只能使用类似于match的语法来进行定义,而函数宏则可以接收一个TokenStream作为参数,与自定义derive宏和属性宏一样,可以在定义中使用rust代码来操作TokenStream,例如下面这个sql!宏:
letsql=sql!(SELECT*FROMpostsWHEREid=1);
sql!宏的签名是这样的:
#[proc_macro]pubfnsql(input:TokenStream)-TokenStream{}
函数宏与自定义derive宏的签名类似,接收TokenStream作为参数,然后返回生成的代码,所以你发现了,声明宏就是用类似正则匹配的方式,而三种过程宏都是对rust代码的编辑再生成,所以声明宏肯定是没有过程宏灵活的,过程宏也没有声明宏编写起来高效,本篇只是简单介绍了宏的能力,如果想更加深入,可以去阅读宏小册[1]
最后本篇是rust入门系列的最后一篇,希望大家对rust有了基本的了解,后面可能会有一些小demo的文章供大家练手,本人也在学习过成功,所以希望可以和大家多多交流,共同进步吧^_^。
封面图:by铁柱呆又呆[2]