【译】Rust宏:教程与⽰例(⼀)
在本⽂中,我们将会涵盖你需要了解的关于 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);
}
这段代码创建了⼀个宏来对两个数进⾏相加。与宏的名称,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
// 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表达式像参数⼀样,我们需要创建⼀个新的宏,被称为。
macro_rules! add{
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 结构体来获取结构体的名字,结构体的字段以及字段类型。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论