likes
comments
collection
share

Rust中的宏:声明宏和过程宏

作者站长头像
站长
· 阅读数 13

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刘金,转载请注明原文链接。感谢!