rust宏(macro)详解

前言

rust 学习曲线非常陡峭,但是基本语法也还算挺好理解,自动内存管理有点类似智能指针,基本看一下语法入门就可以大概理解,但是唯独宏很难理解,语法非常晦涩。但是功能非常强大。声明宏类似于c语言的宏处理,但是功能更强大。过程宏则类似于Android的注解编程,自定义AbstractProcessor,但是实现更优雅。

下面记录一下宏处理的一些特点

正文

目前主流的文章都是翻译自官方文档,或者取部分Rust语言圣经,关键的部分特性就确实,只有rust宏详解中非常详细的介绍,这里简要记录一下特点

声明宏

声明宏主要是替代,主要是通过简单的模式匹配,然后进行操作,这貌似非常容易处理面向对象的工厂模式,或者解决方法多参数操作,比如

rust 复制代码
macro_rules! vec {
    () => (
        $crate::__rust_force_expr!($crate::vec::Vec::new())
    );
    ($elem:expr; $n:expr) => (
        $crate::__rust_force_expr!($crate::vec::from_elem($elem, $n))
    );
    ($($x:expr),+ $(,)?) => (
        $crate::__rust_force_expr!(<[_]>::into_vec(
            // This rustc_box is not required, but it produces a dramatic improvement in compile
            // time when constructing arrays with many elements.
            #[rustc_box]
            $crate::boxed::Box::new([$($x),+])
        ))
    );
}

这就是根据不同的匹配模式,=>的前半部分,替换成后半部。比如无参数的vec。因为这是系统接口,这里不在详细介绍操作,只介绍匹配,

  1. ()是无参构造函数。
  2. ($elem:expr; $n:expr)这是匹配模式类似vec![1;5],这是创造一个size是5的,值是1的vec。
  3. 这个是匹配vec![1, 2, 3],这是构造一个内容是1、2、3的vec

第二个匹配模式中,$elem是为匹配的内容命名,方便后面使用,expr(一个表达式 (expression))指明匹配的元素,;就不用解释,就是字面值。$n:expr同样道理。

第二个匹配则稍微复杂一些,这里则用的是循环模式。循环是通过$(....)来指明的,括号内为循环内容,为了方便阅读,则需要有分隔符和循环次数,这里是通过定义分隔符,+定义循环至少一次。$(,)?又是一个循环,循环内容则是。而循环一次则是最多一次。

所有的语句如下:

block:一个块(比如一块语句或者由大括号包围的一个表达式)

expr:一个表达式 (expression)

ident:一个标识符 (identifier),包括关键字 (keywords)

item:一个条目(比如函数、结构体、模块、impl 块)

lifetime:一个生命周期注解(比如 'foo、'static)

literal:一个字面值(比如 "Hello World!"、3.14、'🦀')

meta:一个元信息(比如 #[...] 和 #![...] 属性内部的东西)

pat:一个模式 (pattern)

path:一条路径(比如 foo、::std::mem::replace、transmute::<_, int>)

stmt:一条语句 (statement)

tt:单棵标记树

ty:一个类型

vis:一个可能为空的可视标识符(比如 pub、pub(in crate))

循环则如下:

反复捕获的一般形式为 $ ( ... ) sep rep。

$ 是字面上的美元符号标记

( ... ) 是被反复匹配的模式,由小括号包围。

sep 是可选的分隔标记。它不能是括号或者反复操作符 rep。常用例子有 , 和 ; 。

rep 是必须的重复操作符。当前可以是:

  1. ?:表示最多一次重复,所以此时不能前跟分隔标记。
  2. *:表示零次或多次重复。
  3. +:表示一次或多次重复。

过程宏

分为三类

  • 派生宏(Derive macro):用于结构体(struct)、枚举(enum)、联合(union)类型,可为其实现函数或特征(Trait)。
  • 属性宏(Attribute macro):用在结构体、字段、函数等地方,为其指定属性等功能。如标准库中的#[inline]、#[derive(...)]等都是属性宏。
  • 函数式宏(Function-like macro):用法与普通的规则宏类似,但功能更加强大,可实现任意语法树层面的转换功能。

声明宏需要解析传入的参数,进行匹配,而过程宏则需要自己解析传入的内容,然后进行补充,生成代码。这里需要解析TokenStream,举个例子,就是用宏为一个结构体实现构建者模式。

rust 复制代码
#[derive(Builder)]
struct Command {
    // ...
}

最麻烦的是如何实现Builder

rust 复制代码
#[derive(Builder)]
struct Command {
	input_paht: String,
    // ...
}


pub fn derive_builder(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput); // 解析input为 DeriveInput类型
    let input_ident = input.ident;  // 获取原始类名
    let ident_builder = format_ident!("{}Builder", input_ident.to_string()); // 拼接builder类名

    if let Data::Struct(r) = input.data {   // 处理结构体
        let fields = r.fields;

        // 结构体属性声明
        let builder_fields = map_fields(&fields, &mut |(ident, ty)| {
            quote!(
                #ident: Option<#ty>,
            ) 
        });

        // 为builder增加set函数
        let builder_set_fields = map_fields(&fields, &mut |(ident, ty)| {
           quote!(
                pub fn #ident(mut self, value: #ty) -> Self {
                    self.#ident = Some(value);
                    self
                }
           ) 
        });

        // 获取builder的属性值
        let builder_lets = map_fields(&fields, &mut |(ident, _)| {
            quote!(
                let #ident = self.#ident.ok_or(format!(
                    "field {:?}  not set yet", stringify!(#ident),
                ))?;
            )
        });

        // 初始化时的默认值
        let builder_fields_values = map_fields(&fields, &mut |(ident, _)| {
            quote!(
                #ident,
            )
        });

        quote!(
            impl #input_ident {
                pub fn builder() -> #ident_builder {
                    #ident_builder::default()
                }
            }

            #[derive(Default)]
            pub struct #ident_builder {
                #builder_fields
            }

            impl #ident_builder {
                #builder_set_fields
                pub fn build(self) -> Result<#input_ident, String> {
                    #builder_lets
                    Ok(#input_ident{ #builder_fields_values })
                }
            }
        ).into()

    } else {
        // 不支持非struct类型
        quote!().into()
    }
}

fn map_fields<F>(fields: &Fields, mapper:&mut F) -> TokenStream2
where
    F: FnMut((&Option<proc_macro2::Ident> ,  &Type)) -> TokenStream2,
{
    let fs = fields.iter().map(|field| mapper((&field.ident ,&field.ty)) );
    let stream2 = TokenStream2::from_iter(fs);
    stream2
}

这里为Command实现了builder方法如下:

rust 复制代码
impl Command{
	pub fn builder() -> CommandBuilder{
	CommandBuilder::default()
	}
}

pub struct CommandBuilder{
    input_path: String,
}

impl CommandBuilder{
     pub fn (mut self, value: String) -> Self {
         self.input_path = Some(value);
         self
     }
     pub fn build(self) -> Result<Command, String> {
          let input_path= self.input_path.ok_or(format!(
                    "field {:?}  not set yet", stringify!(input_path),
           ))?;
          Ok(Command{ input_path })
          }
    }

属性宏则可以传入参数,让控制更自由一些,这里就不在详细介绍

函数式宏则相对比较简单,类似声明宏,但是可以不去匹配规则,更自由,功能更强大。

解析TokenStream需要依赖一些库,这比较复杂,就不在详细介绍。要结合自己代码需求,慢慢理解。

分析工具
bash 复制代码
cargo.exe  install cargo-expand

cargo.exe expand

后记

rust实在是复杂,这里解释一些语法规则,以后遇到问题再补充。

相关推荐
一颗花生米。12 分钟前
深入理解JavaScript 的原型继承
java·开发语言·javascript·原型模式
问道飞鱼12 分钟前
Java基础-单例模式的实现
java·开发语言·单例模式
学习使我快乐0116 分钟前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
通信仿真实验室1 小时前
(10)MATLAB莱斯(Rician)衰落信道仿真1
开发语言·matlab
勿语&1 小时前
Element-UI Plus 暗黑主题切换及自定义主题色
开发语言·javascript·ui
吾爱星辰5 小时前
Kotlin 处理字符串和正则表达式(二十一)
java·开发语言·jvm·正则表达式·kotlin
ChinaDragonDreamer5 小时前
Kotlin:2.0.20 的新特性
android·开发语言·kotlin
IT良5 小时前
c#增删改查 (数据操作的基础)
开发语言·c#
Kalika0-06 小时前
猴子吃桃-C语言
c语言·开发语言·数据结构·算法
_.Switch6 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j