Rust中的宏:声明宏和过程宏
Rust中的声明宏和过程宏
宏是Rust语言中的一个重要特性,它允许开发人员编写可重用的代码,以便在编译时扩展和生成新的代码。宏可以帮助开发人员减少重复代码,并提高代码的可读性和可维护性。Rust中有两种类型的宏:声明宏和过程宏。
声明宏:
声明宏是一种用于定义新的宏的语法。它使用macro_rules!
关键字定义,并遵循特定的语法规则。声明宏通常用于定义简单的宏,例如计算两个数字之和或打印一条消息。
例如,下面是一个简单的声明宏,用于计算两个数字之和:
macro_rules! add {
($x:expr, $y:expr) => {
$x + $y
};
}
fn main() {
let x = 5;
let y = 6;
println!("{}", add!(x, y));
}
在上面的示例中,我们定义了一个名为add
的声明宏。该宏接受两个参数:$x
和$y
,并使用=>
符号将参数映射到表达式$x + $y
。在主函数中,我们使用add!(x, y)
调用该宏,并将结果打印到控制台。
macro_rules! 中有一些奇怪的地方,所以在将来会有用 macro 关键字的声明宏,其工作方式类 似但修复了这些极端情况。声明宏这种方式将会被废弃。目前我认为就是使用过程宏中的函数宏代替声明宏的使用。
过程宏:
定义过程宏的函数接收一个 TokenStream 作为输入并生成 TokenStream 作为输出。
过程宏是另一种用于定义新的宏的语法。与声明宏不同,过程宏使用特殊的函数来定义,并可以接受任意数量的参数。过程宏通常用于定义更复杂、更强大的宏,例如实现自定义派生或生成新的类型。
因为它们更像函数(一种过程类型)。所以被称为过程宏(procedural macros),过程宏接收 Rust 代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出,而非像声明式宏那样匹配 对应模式然后以另一部分代码替换当前代码。
有三种类型的过程宏
-
自定义派生(derive)宏
自定义派生(Custom Derive)允许我们为指定的类型自动生成trait实现。例如,我们可以使用#[derive(Debug)]
属性来自动生成Debug
trait的实现。
下面是一些关于自定义派生宏和属性宏的代码示例。
首先,我们来看一个自定义派生宏的示例。在这个示例中,我们将定义一个名为HelloWorld
的trait,以及一个名为HelloWorldDerive
的过程宏,用于自动生成HelloWorld
trait的实现。
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
trait HelloWorld {
fn hello_world(&self);
}
#[proc_macro_derive(HelloWorld)]
pub fn hello_world_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
let expanded = quote! {
impl HelloWorld for #name {
fn hello_world(&self) {
println!("Hello, World! My name is {}", stringify!(#name));
}
}
};
TokenStream::from(expanded)
}
#[derive(HelloWorld)]
struct MyStruct;
fn main() {
let my_struct = MyStruct;
my_struct.hello_world();
}
在上面的示例中,我们定义了一个名为HelloWorld
的trait,该trait包含一个名为hello_world
的方法。然后,我们使用proc_macro_derive
属性来定义一个名为hello_world_derive
的过程宏,该宏用于自动生成HelloWorld
trait的实现。最后,我们使用#[derive(HelloWorld)]
属性来为MyStruct
类型自动生成HelloWorld
trait的实现。
但是自定义宏有一个局限性是只能用在结构体和枚举类型上,所以如果想用在其他的上面,例如函数上面,要使用类属性宏。
- 类属性宏.
属性(Attribute)允许我们在函数、模块或者项上添加自定义属性。例如,我们可以使用#[test]
属性来标记测试函数。
看一个属性宏的示例。在这个示例中,我们将定义一个名为show_streams
的属性宏,用于打印函数的输入和输出TokenStream。
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
#[proc_macro_attribute]
pub fn show_streams(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let name = input.sig.ident;
let block = input.block;
let output = quote! {
fn #name() {
println!("Before the function");
#block
println!("After the function");
}
};
output.into()
}
#[show_streams]
fn my_function() {
println!("In the function");
}
fn main() {
my_function();
}
在上面的示例中,我们使用了proc_macro_attribute
属性来定义一个名为show_streams
的属性宏。该宏接受一个函数作为输入,并在函数执行前后分别打印一条消息。然后,我们使用#[show_streams]
属性来应用该宏到一个名为my_function
的函数上
基本上类属性宏我认为可以解决所有的可以用宏的问题。也是功能最强大的宏。
-
类函数宏
函数(Function-like)则类似于声明宏,允许我们定义新的语法结构。不同的是,函数过程宏接受一个TokenStream作为输入,并返回一个新的TokenStream作为输出。
下面是一个简单的函数过程宏示例,用于定义一个名为double
的宏,该宏接受一个表达式参数并返回该表达式的两倍值。
use proc_macro::TokenStream;
#[proc_macro]
pub fn double(input: TokenStream) -> TokenStream {
let input_str = input.to_string();
let expr = syn::parse_str::<syn::Expr>(&input_str).unwrap();
let output = quote::quote! {
2 * #expr
};
output.into()
}
fn main() {
let x = 3;
let y = double!(x);
println!("{}", y);
}
在上面的示例中,我们使用了proc_macro
crate来定义一个名为double
的函数过程宏。该宏接受一个表达式参数,并使用该参数生成一个新的表达式,表示该表达式的两倍值。
它和声明宏的工作方式类似,可以用来代替声明宏。
声明宏和过程宏的比较:
声明宏和过程宏都可以用于定义新的宏,但它们之间存在一些差异。声明宏更简单、易于使用,但功能有限;而过程宏更强大、灵活,但需要更多的编码技巧。
例如,在上面给出的示例中,我们可以看到声明宏和过程宏都可以用于计算 两个数字之和。但是,声明宏只能接受固定数量的参数,并且必须遵循特定的语法规则。而过程宏则可以接受任意数量的参数,并且可以使用任意的Rust代码来定义宏的行为。
此外,声明宏和过程宏在实现方式上也有所不同。声明宏是在编译时扩展的,这意味着它们在编译器内部被处理。而过程宏则是在编译时调用的,这意味着它们在编译器外部被处理。这种差异使得过程宏可以访问更多的编译器信息,并且可以使用更复杂的算法来生成新的代码。
宏和函数的比较
既然宏可以做到函数想做到的所有事情,为什么我们不直接使用宏去代替代码中的函数呢?
- 宏提供了类似函数的功能,但是没有运行时开销。但是,因为宏会在编译期进行展开(expand),所以它会有一些编译期的开销,导致编译时间变长。除非想用编译时间代替运行时间,来提高用户体验,可以这么做。
- 在实时使用的代码上面可能无法使用宏,为宏会在编译期进行展开(expand),如果一些参数如果是在使用时才输入,就无法在编译时进行宏的编写运行。
- 在代码编写上,函式的编写要比宏的编写更为简单易读。这也是宏没有代替方法的原因。
结论:
总之,Rust中的宏是一种强大的工具,可以帮助开发人员编写可重用、高效和灵活的代码。无论是声明宏还是过程宏,都值得开发人员学习和掌握。通过使用宏,开发人员可以减少重复代码,并提高代码的可读性和可维护性。因此,如果您正在使用Rust语言进行软件开发,那么了解宏是非常重要的。from刘金,转载请注明原文链接。感谢!