Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

我们在本书中已经使用过宏,比如 println!,但我们还没有完全探讨宏是什么以及它们是如何工作的。宏(Macro)指的是 X 语言中的一系列功能:声明宏(declarative macros)与 macro! 和三种程序宏(procedural macros):

  • 自定义 #[derive] 宏,用于在结构体和枚举上使用 derive 属性指定代码
  • 类属性宏,用于定义可用于任意项目的自定义属性
  • 类函数宏,看起来像函数调用,但对作为其参数指定的标记进行操作

让我们按顺序讨论每一个,但首先,让我们看看为什么我们需要宏,而我们已经有了函数。

宏和函数的区别

从根本上说,宏是一种编写其他代码的代码方式,这被称为元编程(metaprogramming)。在附录 C 中,我们探讨了 derive 属性,它会自动为你生成各种 trait 的实现。我们在本书中也使用了 println!format!。所有这些宏都通过展开(expand)来生成比你手动编写的代码更多的代码。

元编程对于减少你必须编写和维护的代码量很有用,这也是函数的作用之一。然而,宏有一些函数没有的额外能力。

函数签名必须声明它们有多少个参数以及这些参数的类型。另一方面,宏可以接受可变数量的参数:我们可以用一个参数调用 println!("hello") 或用两个参数调用 println!("hello {}", name)!此外,宏可以在编译时展开,因此宏可以,例如,在编译时实现一个类型的 trait。函数不能这样做,因为它们在运行时被调用,而 trait 需要在编译时实现。

宏的一个缺点是,与函数定义相比,宏定义更难阅读,因为你正在编写编写 X 语言代码的 X 语言代码。由于这种间接性,宏定义可能比函数定义更难阅读、理解和维护。

宏和函数之间的另一个重要区别是,你必须在调用宏之前定义宏或将它们带入作用域,而你可以在任何地方定义函数并在任何地方调用它们。让我们开始探索声明宏!

声明宏与 macro! 用于通用元编程

X 语言中最常用的宏形式是声明宏(declarative macros)。它们有时也被称为“示例宏“(macros by example)、macro_rules! 或简称为“macros“。在它们的核心,声明宏允许你编写类似于 match 表达式的东西。正如我们在第 6 章中讨论的,match 表达式是控制流结构,它接受一个值,将该值与模式进行比较,然后运行与匹配模式相关联的代码。宏也将一个值与具有相关代码的模式进行比较:在这种情况下,该值是字面 X 语言源代码,模式与源代码的结构进行比较,并且与每个模式相关联的代码在匹配时替换传递给宏的代码。这一切都发生在编译时!

要定义宏,你使用 macro_rules!。让我们看看 vec! 宏是如何定义的,作为一个例子。第 8 章讨论了我们如何使用 vec! 宏来创建一个包含特定值的新列表。例如,下面的宏创建一个包含三个整数的新列表:

let v: List<integer> = vec![1, 2, 3]

我们可以使用这个宏来创建一个包含任意数量任意类型值的列表。我们可以使用 macro_rules! 来定义 vec! 宏。让我们看看 vec! 的简化定义是什么样子的。示例显示了 vec! 宏的稍微简化的定义。

macro vec {
  ($($x:expr),*) => {
    {
      let mut temp_vec = List::new()
      $(
        temp_vec = temp_vec + [$x]
      )*
      temp_vec
    }
  }
}

注意:vec! 的实际标准库定义包括预分配正确数量内存的代码,作为优化。我们没有在这里包含该代码,以保持示例简单。

macro vec! 声明开始了宏定义。然后我们有一组大括号,表示宏定义的主体。vec! 宏的结构类似于 match 表达式的结构。我们有一个臂,其中包含模式 ($($x:expr),*),后跟 => 和与该模式相关联的代码块。如果模式匹配,这个代码块将被执行。鉴于这是这个宏中唯一的模式,只有一种有效的匹配方式;任何其他模式都会导致错误。更复杂的宏会有多个臂。

宏模式中的语法与 match 模式中的语法不同,因为我们正在匹配 X 语言代码结构而不是值。让我们逐步了解示例中模式的部分。对于完整的宏语法,请参阅 X 语言参考。

首先,一组括号包围了整个模式。我们在 $() 内部使用了 $x:expr,它匹配任何 X 语言表达式,并将该表达式绑定到元变量 $x$() 后面的逗号表示字面逗号字符可以可选地出现在匹配 $() 中的代码的代码之后。* 指定模式匹配零次或多次前面的任何内容。

当我们用 vec![1, 2, 3] 调用这个宏时,$x 模式与三个表达式 123 匹配了三次。

现在让我们看看与这个臂相关联的代码块中的模式:temp_vec$()* 内的 temp_vec = temp_vec + [$x] 部分为每次匹配零次或多次的部分在展开时生成;每次生成与该迭代匹配的 $x。当我们用 vec![1, 2, 3] 调用这个宏时,替换此宏的代码变成了这个:

{
  let mut temp_vec = List::new()
  temp_vec = temp_vec + [1]
  temp_vec = temp_vec + [2]
  temp_vec = temp_vec + [3]
  temp_vec
}

我们已经添加了一个简单的宏,它可以用任意数量的任意类型表达式创建一个列表。macro_rules! 有一些奇怪的边缘情况。在某个时候,X 语言可能会引入一种用于声明宏的第二个宏语法,它的工作方式类似但修复了这些边缘情况。届时,macro_rules! 将被有效地弃用。考虑到这一点,以及大多数 X 语言程序员使用宏而不是编写宏这一事实,我们不会在这个主题上花更多时间。有关更多信息,请参阅文档或其他来源,如《X 语言宏小手册》。

好的,现在我们已经了解了声明宏,让我们继续讨论三种程序宏!