天天看点

Rust : 宏、重复、及向量与递归实例

Rust的宏功能强大,绝对不是花架子以及一堆丑陋的代码堆而已,它值得你花点时间。Rust宏中,有一个难点就是重复(repetition)功能。这里想主要阐述一下。

关于宏相关的高级的资料,可以参考《Rust宏小册 中文版》,具体资料见:https://daseinphaos.github.io/tlborm-chinese/book/README.html。

一、重复的模式

重复的模式需要用括号括起来,外面再加上 $。括号里面,是标记一个模式的开始和结束,往往是用逗号或分号分隔的。

几种主要的重复模式,基本上就是“,”“;”,“+”,“*”的组合模式。

一种组合,这种情况更多放在宏的执行动作中,其中“,”、“;”看场景,有时侯也可以不用。

$(...,)*   
$(...;)*
$(...,)+  
$(...;)+
           

另一种组合:往往放在宏的参数()中,表明各输入参数是以“;”还是“,”隔开。

$(...),*   //参数以“,”隔开,类似: let mut t =vec![1,2,3];
 $(...);*   //参数以“;”隔开,类似: let mut t =vec![1;2;3]; 以下类推.
 $(...),+  
 $(...);+
           

其中,

+  表示一次或多次(至少一次),而  *  表示零次或多次。
 () 括号,也可以用[],{}代替,只不过最好按惯用的规则,更清晰。
           

二、类似于vec!的例子

//version 1.0
macro_rules! vector {
($($x:expr),*) => {             // 1、为什么这里面用“,”号?
    {
       let mut temp_vec = Vec::new();
       $(temp_vec.push($x);)* // 2、还有其它的写法? 3、为什么这里用“;”
       temp_vec
    }
};
}
           

上面2个问题:

(1)、关于问题1,用“,”的目的是什么?我们知道,

let mut temp =vec![1,2,3]; // 当然也可以 let mut temp =vec!(1,2,3); 也可以用{}。
           

中,1,2,3是用“,”分隔的。如果用“;”那么,这个分隔号就必须用“;”。那如果我“,”或“;”都可以用呢? 你可以选择改一下上面的代码。

//version 2.0
macro_rules! vector {
($($x:expr),*) => {
    {
       let mut temp_vec = Vec::new();
       $(temp_vec.push($x);)*
       temp_vec
    }
};
($($x:expr);*) => {
    {
       let mut temp_vec = Vec::new();
       $(temp_vec.push($x);)*
       temp_vec
    }
};
}
           

需要说明的是,即使如此:

let mut data =vector![1,2;3]; // 这种“,”、“;”混用,仍然是不行的;
           

(2)、关于问题2

除“ * ”可以改成“+”外,

$(temp_vec.push($x);)* //这个模式和前四种不一样呀
           

还可以:

//version 3.0
macro_rules! vector {
($($x:expr),*) => {
    {
       let mut temp_vec = Vec::new();
       ($(temp_vec.push($x)),*);  // 这里的“,”不能用“;”替代。
       temp_vec
    }
};
}
           

另外,来一个简化版的,去掉上面的括号:

//version 4.0
macro_rules! vector {
($($x:expr),*) => {
    {
       let mut temp_vec = Vec::new();
       $(temp_vec.push($x)),* //简化版的,“*”可以换成“+”
       temp_vec
    }
};
}
           

此外,在version3.0的基础上,写一个可以“;”的版本。version3.0的版本不能用“;”的原因,很可能是让编译器产生歧义,因此,我们可以改写一下:

//version 5.0
macro_rules! vector {
($($x:expr),*) => {
    {
       let mut temp_vec = Vec::new();
       $(temp_vec.push($x));*;  // 这里可以用“;”或“,”,因为结尾加了一个“;”
       temp_vec
    }
};
}
           

( 3 )、“;”不可以换成“,”

(4)、注意,宏的重复语句可以不用“;”结尾

$(temp_vec.push($x);)*;  // 结尾“;”可以省略
      或
      $(temp_vec.push($x)),+;  // 同样,结尾“;”可以省略
      
           

三、一个递归(recursion)的例子

我们再来看下面的一个例子(注:来源rustprimer)。借用这个例子的原因是这个例子有很多值得分析的地方。

macro_rules! find_min {
   ($x:expr) => ($x);
   ($x:expr, $($y:expr),+) => (              // 1、第一个逗号和第二个逗号 
       std::cmp::min($x, find_min!($($y),+)) // 2、find_min!($($y),+))还有其它的写法?
   )
}
// 下面的说明也来源自rustprimer.
因为模式匹配是按分支顺序匹配的,一旦匹配成功就不会再往下进行匹配(即使后面也能匹配上),所以
模式匹配中的递归都是在第一个分支里写最简单情况,越往下包含的情况越多。这里也是一样,第一个分
支  ($x:expr)  只匹配一个表达式,第二个分支匹配两个或两个以上表达式,注意加号表示匹配一个或多个,然后里面是用标准库中的  min  比较两个数的大小,第一个表达式和剩余表达式中最小的一个,其中剩余表达式中最小的一个是递归调用  find_min!  宏,与递归函数一样,每次递归都是从上往下匹配,只到匹配到基本情况。我们来写写  find_min!(5u32, 2u32 * 3, 4u32)  宏展开过程
1.  std::cmp::min(5u32, find_min!(2u32 * 3, 4u32))
2.  std::cmp::min(5u32, std::cmp::min(2u32 * 3, find_min!(4u32)))
3.  std::cmp::min(5u32, std::cmp::min(2u32 * 3, 4u32))
分析起来与递归函数一样,也比较简单。
           

上面的2个问题:

1、问题1

第一个逗号能换成分号么 ?

当然可以,但是,要改的不是一点了。这相当于需要编译器接受“;”为分隔号。

接受比如以下的格式:

let mut data =find_min2[1;2;3];
           

具体的改法如下:

macro_rules! find_min2 {
   ($x:expr) => ($x);
   ($x:expr; $($y:expr);*) => (                 //两个“;”分隔号
       std::cmp::min($x, find_min2!($($y);*))   // 这里也要改成“;”分隔号,而且不能用“,”
   )
}
           

第二个逗号能去掉么? 不行,因为你这里要告诉编译器,你接受的参数是用什么来进行分隔的。

2、问题2

当然,上面的例子,直接把“+”换成“*”是没有问题的;

除此之外,能否换成其它模式? 比如下面把其中的","去掉,形成简化的形式?

find_min!($($y),+) =>    find_min!($($y)+)
           

我认为:不可以(没有找到其它的成功的案例)。因为在这里find_min!()内的参数形式,要有参数”分隔“的形式感。

四、订制化的println

macro_rules! my_println {
    () => {
        println!("macro, my println!");
    };
    ($val:expr) => {
        println!("Look at this other macro: {}", $val);
    };
    ($($x:expr),*) => {
        $(println!("{}",$x);)* 
    };
}
fn main() {
    my_println!(1, 2);
    thread::sleep_ms(500000);
}
           

输出是:

1

2