Mastering_Rust(译):宏(第八章)(完+1)

Rust支持几种形式的元编程,这意味着编写编写程序的程序。 它可以是一种非常强大的技术,有助于超越语言本身的局限性。 然而,这是一种相当具有挑战性的编程方式,与编写常规函数相比,需要更多的关注和考虑。

Rust中最古老,最稳定的元编程形式是语法宏。 我们将介绍本章中的内容。

本章将介绍以下主题:

  • 元编程简介
  • 解剖println!
  • 宏关键字
  • 重复构造
  • 构建我们自己的宏

元编程简介

在理想和简化的形式中,编程由两个明显分开的东西组成:程序代码和数据。 一旦你完成了你的代码,它就像石头雕刻,不可塑。

元编程意味着编写编写程序的程序。 不同的编程语言如何做到这一点。 例如,C有一个预处理器,它以#开头读取特定的标签,并在将结果传递给实际的编译器之前展开它们。 在C中,这些扩展是完全自由的; 它们只是简单的文本转换而没有太多安全性。 具体来说,用C(和其他一些语言)编写的宏不卫生:它们可以引用任何地方定义的变量,只要这些变量在宏调用站点的范围内。 例如,这是一个切换两个参数的宏:

* switcher.c */
#include <stdio.h>
#define SWITCH(a, b) { temp=b; b=a; a=temp; }
int main() {
int x=1;
int y=2;
int temp=3;
SWITCH(x, y);
printf("x is now %d y is now %d temp is now %d\n", x, y, temp);
}

由于宏调用只是替换文本,因此使用temp变量的SWITCH工作正常。 这种不卫生的性质使得宏虽然危险而脆弱; 除非采取特殊预防措施,否则它们很容易弄得一团糟。 我们强调卫生的概念,因为Rust宏是卫生的,而且比简单的字符串扩展更有条理。

Rust有几种类型的元编程和一些即将推出的元编程。 由于Kohlbecker和Wand在1986年引入该技术的论文,最稳定的形式是语法宏,也称macros-by-example,它们由另一个宏定义,称为macro_rules!。 这些宏在编译器的抽象语法树输出中与其他程序代码一起表示。 这意味着这些宏不能在代码中的任何地方使用,而只能代替方法,语句,表达式,模式和项。 它进一步意味着宏的参数必须在AST内良好形成; 它们必须是格式良好的标记树。

另一种形式的语法扩展称为过程宏或编译器插件。 它们比语法宏更强大,允许在编译过程中运行任何自定义Rust代码。 价格是它们实现起来更复杂,并且依赖于编译器内部,以至于它们可能永远不会在稳定的Rust中完全实现。

正在构建一种有限形式的过程宏,称为宏1.1。 这样做的动机是程序宏被许多备受瞩目的库(例如流行的序列化框架,Serde)有效地使用,基本上使它们只在夜间Rust中正常工作。 然而,发现大多数这些库仅使用整个过程宏机器的有限子集。 宏1.1试图实现该子集,从而可以在稳定的Rust中使用具有全部功能的库。 这项工作已经顺利进行,并可能在2016年底前稳定发布。

我们将在下一章中介绍两种形式的过程宏。

为了使未来更加迷人,逐个宏的系统也正在改进,但这项工作预计需要更长的时间才能完成。

一般而言,元编程和宏都是有效的工具,但也是危险的; 它们可以轻松地使代码更脆弱,并且不易于读取和调试。 因此,只有在考虑到更稳定的(例如功能,特征和仿制药)并认为不足之后,才应使用这些技术。 你已经多次见过的一个例子是println!宏。 它已被实现为一个宏,因为它允许Rust在编译时检查其参数是否有效。 如果打印! 是常规功能,这是不可能的。 请考虑以下示例:

println("The result of 1+1 is {}");
println!("The result of 1+1 is {}");

如您所知,第二种形式将在编译时失败,因为它缺少与格式字符串匹配的参数。 对于该函数,无法在Rust中进行此编译时检查。 此外,Rust不支持具有可变数量参数的函数。

解剖println!

让我们从潜入深处开始:我们将带走我们的老朋友println!分开。 以下是标准库中的定义,没有实际的代码体:

macro_rules! println {
($fmt:expr) => (print!(concat!($fmt, "\n")));
($fmt:expr, $($arg:tt)*) => (print!(concat!($fmt, "\n"), $($arg)*));
}

macro_rules! 创建新的宏。 它的第一个参数是新宏的名称,然后它遵循模式匹配的代码体。 以 西 开头的东西(例如前面定义中的 fmt:expr)将被赋予任何自由格式字符串在其位置,并且其他所有内容(例如前面定义中的逗号)将被逐字解析。 在println的情况下,有两个匹配:

  • ($ fmt:expr)匹配单个表达式,该表达式进入变量$ fmt。
  • ($ fmt:expr, arg:tt)*)匹配单个表达式,后跟逗号,后跟零个或多个参数。 参数存储在$ arg中。

expr和tt都是特殊关键字,表达式和标记树的缩写。 我们稍后会看到它们的具体含义。 让我们举一个例子,println!的调用,匹配第二个模式。 无论如何,第一种情况几乎是相同的。 这是我们将要开始的地方:

println!("Help, I'm {} a {}!", "inside", "macro")

按顺序尝试模式,匹配的第一个模式被选中。 第一个模式不匹配,因为宏的参数在第一个表达式后面有逗号,但模式没有。 第二个将匹配得很好,所以整个表达式扩展为:

print!(concat!("Help, I'm {} a {}!", "\n"), "inside", "macro")

我们需要看一下concat的定义! 看看接下来会发生什么。 在这里,它直接来自源代码:

macro_rules! concat { ($($e:expr),*) => ({ /* compiler built-in */ }) }

好的,这没什么用。 我们可以看到它需要用逗号分隔的任意数量的表达式,但它的实现被焊接到编译器中。 我们只需要相信concat!的文档,它声明它将所有文字参数连接成一个静态字符串。 这意味着下一次扩展将成为:

print!("Help, I'm {} a {}!\n", "inside", "macro")

定义print!宏是:

macro_rules! print {
($($arg:tt)*) => ($crate::io::_print(format_args!($($arg)*)));
}

我们走了兔洞! 我们的表达成为:

$crate::io::_print(format_args!("Help, I'm {} a {}!\n", "inside", "macro"))

我们差不多了,因为format_args! 又是一个内置的编译器:

macro_rules! format_args { ($fmt:expr, $($args:tt)*) => ({
/* compiler built-in */
}) }

现在,就宏观扩张而言,我们触底了。 format_args! macro是标准库中需要格式化功能的所有其他宏最终调用的内容。 它返回std :: fmt :: Arguments类型中的格式化参数。 从本质上讲,它将是一个安全解析的字符串版本,帮助,我在一个宏!,但包裹在该类型。

format_args! 宏,因此每个使用它的宏都会进行语法检查。 例如,如果我们尝试使用println! 如果参数数量错误,我们会收到错误:

fn main() {
println!("I have two parameters {} {}, but am only supplied one", 1);
}

编译器报错:
在这里插入图片描述

编译器错误并没有准确地指出我们做错了什么,但至少告诉我们出了问题。 此外,当我们有太多参数时,我们得到一个编译时错误:

fn main() {
println!("I have two parameters {} {}, but am supplied with three", 1, 2, 3);
}

相应的编译器输出是:
在这里插入图片描述

整个宏扩展过程,以及所有相关的错误检查,都发生在编译时。对于println没有什么特别和特权! 除了少数编译器内置插件。 您可以使用相同的机制,接下来您将看到如何构建自己的宏。

演习

为什么要打印! 需要两种模式?

2.为什么是println! 一个宏而不仅仅是一个函数?

3.想想你的第二个最喜欢的编译语言。 它如何与println进行相同的检查! 通过宏? 要么选择优越吗?

调试宏

在我们讨论宏关键字并构建自己的宏之前,让我们看一下当宏不起作用时该怎么做。 第一种技术是要求编译器在宏扩展完成后向我们展示代码。 这是我们的宏,无论是什么都没有或块。 作为奖励,让我们看看println!真正变成了什么:

// expand-macro.rs
macro_rules! meep {
() => (nothing);
($block:block) => ( make($block); );
} f
n main() {
meep!();
meep!({silly; things});
println!("Just to show how fun println! really gets");
}

通过使用参数–pretty expanded从编译器请求扩展。 这是一个不稳定的特性,但在编写本书时,它仍然受到稳定编译器的支持。 从编译器输出中可以看出,这可能会很快改变,分为三个部分:
在这里插入图片描述

这是编译器输出的错误部分,显示宏代码及其调用中未解析的名称:
在这里插入图片描述

最后,这是println的扩展宏输出!:

在这里插入图片描述

因此,我们可以看到,只要我们的宏及其调用不会破坏解析规则太严重,我们就可以看到扩展的代码。 请注意编译器如何足够好地指向实际的宏调用而不是扩展的代码作为错误的来源:

expand-macro.rs:8:12: 8:17 error: unresolved name `silly` [E0425]
expand-macro.rs:8 meep!({silly; things});

如果您需要为整个Cargo项目执行此操作,也可以通过名为cargo-expand的Cargo包装器使用此功能。 但是,我们不会去那里,因为它与前面的事情基本相同。

跟踪宏是另一种调试宏的方法。 它们具有门控功能,这意味着它们只能在nightly 版Rust一侧使用。 有两个这样的宏:

  • trace_macros! 采用布尔值并全局打开或关闭宏跟踪
  • log_syntax! 只需在编译时输出所有参数
    这是以前的expand-macro.rs,修改为使用trace_macros! 和log_syntax!,并修复了未解析的名称:
// trace-macros.rs
#![feature(trace_macros, log_syntax)]
trace_macros!(true);
macro_rules! meep {
() => ();
($block:block) => (
log_syntax!("Inside 2nd branch, block is" $block);
($block);
log_syntax!("Leaving 2nd branch!");
);
}

主要功能保持不变,因此无需重复。 以下是我们编译时nightly编译器给出的内容:

我们可以看到,log_syntax! 确实按字母顺序输出参数,带引号和所有。 另外,请注意println! 与-pretty扩展编译器输出给出的输出相比,扩展更好一些。

跟踪宏可能是目前和任何可预见的未来调试宏的最佳工具。 因此,如果您决定成为一名认真的宏程序员,请准备好nightly版并使用Rust。

宏关键字

让我们通过查看宏模式可能具有的不同已识别关键字列表来开始编写我们自己的宏的过程:

block:这是一系列语句
expr:这是一个表达式
ident:这是一个标识符
item:这是一个项目
meta:这是一个元项目
pat:这是一种模式
path:这是一个限定名称
stmt:这是一个声明
tt:这是一个令牌树
ty:这是一种类型

block

我们在调试示例中已经使用过block。 它匹配由大括号分隔的任何语句序列,例如我们之前使用的语句:

{ silly; things; }

这个块有silly和things。

expr

这匹配单个表达式,例如:

  • 1 x
  • +1
  • if x==4 { 1 } else { 2 }

值得注意的是,它与let x = 1之类的语句不匹配,因为它不是单个表达式

ident

标识符是任何不是关键字的Unicode字符串(例如if或let)。 作为例外,仅下划线字符不是Rust中的标识符。 标识符示例:

  • x l
  • ongIdentifier
  • SomeSortOfAStructType

item

顶级定义称为项目。 这些包括函数,使用声明,类型定义等。 这里有些例子:

  • use std::io;
  • fn main() { println!(“hello”) }
  • const X: usize = 8;
    当然,这些不必是单行的。 主要功能是单个项目,即使它跨越多行。

meta

属性内的参数称为元项,由元捕获。 属性本身如下:

  • #![foo]
  • #[foo]
  • #[foo(bar)]
  • #[foo(bar=“baz”)]

元项是括号内的东西。 因此,对于前面的每个属性,相应的元项如下:

  • foo
  • foo
  • foo(bar)
  • foo(bar=“baz”)

pat

匹配表达式在每个匹配的左侧都有模式,这些模式可以捕获。 这里有些例子:

  • 1
  • “x”
  • t *
  • t
  • Some(t)
  • 1 | 2 | 3
  • 1 … 3
  • _

path

路径是限定名称,即附加了名称空间的名称。 它们与标识符非常相似,只是它们允许双冒号。 这里有些例子:

  • foo
  • foo::bar
  • Foo
  • Foo::Bar::baz

stmt

语句很像表达式,除了stmt接受更多模式。 以下是一些例子:

  • foo
  • 1 1
  • +2
  • let x = 1

特别是,最后一个不会被expr接受。

tt

tt关键字捕获单个标记树。 标记树是单个标记(例如1,+或“foo bar”)或由任何大括号(),[]或{}包围的多个标记。 以下是一些例子:

  • foo
  • { bar; if x == 2 { 3 } else { 4 }; baz }
  • { bar; fi x == 2 ( 3 ] ulse ) 4 {; baz }
    如您所见,令牌树的内部不必具有语义意义; 他们只需要成为一系列令牌。 具体而言,与此不匹配的是两个或更多未包含在括号中的标记(例如1 + 2)。

ty

ty关键字捕获看起来像类型的东西。 这里有些例子:

  • u32
  • u33
  • String
  • Strong

没有语义检查类型实际上是一个类型是在宏扩展阶段完成的,所以“u33”和“u32”一样被接受。

重复构造

我们只需要一种额外的机制来编写宏:一种模拟重复模式的方法。 我们在vec中看到过这个! 之前的宏:

vec![1, 2, 3]

这将创建并返回具有三个元素的新向量。 让我们看看vec! 可以。 这是它的macro_rules! 定义:

macro_rules! vec {
($elem:expr; $n:expr) => ($crate::vec::from_elem($elem, $n));
($($x:expr),*) => (<[_]>::into_vec(box [$($x),*]));
($($x:expr,)*) => (vec![$($x),*])
}

让我们忽略右侧并关注最后两种模式:

$($x:expr),*
$($x:expr,)*

重复模式匹配遵循以下模式: var:type)。 根据您希望宏调用的外观,可能会有任意数量的字符串文字。 在vec!中,字符串文字是逗号字符。 在第一个匹配中,逗号字符在重复匹配之外。 这是典型的情况,并且将匹配诸如1,2,3之类的序列。但是,它将与具有尾随逗号的序列匹配,例如1,2,3。 这样的序列在格式化时更有意义:

vec![
1,
2,
3,
];

它使宏的用户不必记住从最后一项中删除逗号。 第二个模式捕获重复匹配内的逗号,允许前面的表单。 但是,该模式与1,2,3不匹配,因此我们都需要它们。

重复构造需要以下两个限定符之一,熟悉正则表达式:

  • *表示重复需要发生零次或多次
  • +表示重复需要发生一次或多次

vec中的模式! 使用*,这意味着vec![]是允许的宏调用。 用+,它不会。

现在让我们看看重复在右侧是如何工作的。 有两种使用方法。 vec!宏不需要处理序列本身的每个捕获元素,因此它只是使用相同的语法转发它们:

($($x:expr),*) => (<[_]>::into_vec(box [$($x),*]));

左侧声明和右侧声明之间的唯一区别是右侧不包括变量的类型(expr)。

第二种使用方法是逐个浏览元素。 这个语法类似:我们用$()包含我们想要为每个元素执行的代码并再次限定。 这是一个宏,它输出在编译时给出的所有元素:

#![feature(log_syntax)]
macro_rules! m1 {
($($x:tt),*) => {
$(
log_syntax!(Got $x);
)*
};
} f
n main() {
m1!(Meep, Moop, { 1 2 3 });
}

请注意,我们正在捕获宏模式中的标记树; 编译此代码为我们提供了以下输出:

在这里插入图片描述

现在,我们已经涵盖了通过宏进行元编程所需的几乎所有内容。 我们来看一个示例宏。

示例 - HTTP测试程序

让我们通过处理具有重叠模式的自定义宏来了解宏扩展的功能。 此宏实现了一种小语言,旨在使用超级库描述简单的HTTP GET / POST测试。 以下是没有封闭宏调用的语言示例:

http://google.com GET => 302
http://google.com POST => 411

第一行向Google发出GET请求,并期望返回代码302(已移动)。 第二个请求POST到同一个地方,并期望返回代码411(需要长度)。 这非常简单,但对我们的目的来说已经足够了。

Hyper是Rust事实上的标准HTTP库,它支持服务器和客户端操作。 我们对客户部分感兴趣。 由于它是一个库包,我们需要用Cargo构建一个完整的Rust应用程序,所以我们可以声明依赖。 我们将调用我们的程序http-tester。 这是它的Cargo.toml:

[package]
name = "http-tester"
version = "0.1.0"
authors = ["Vesa Kaihlavirta <[email protected]>"]
[dependencies]
hyper="0.9.*"

这是src / main.rs

extern crate hyper;
use hyper::client::Client;
use hyper::status::StatusCode;
macro_rules! http_test {
($url:tt GET => $code:expr) => {
let client = Client::new();
let res = client.get($url).send().unwrap();
println!("GET {} => {}", $url, $code);
assert_eq!(res.status, $code);
};
($url:tt POST => $code:expr) => {
let client = Client::new();
let res = client.post($url).send().unwrap();
println!("POST {} => {}", $url, $code);
assert_eq!(res.status, $code);
};
} f
n main() {
println!("Hello, world!");
http_test!("http://google.com" GET => StatusCode::Ok);
http_test!("http://google.com" POST => StatusCode::MethodNotAllowed);
http_test!("http://google.com" POST => StatusCode::Ok);
}

正如您所看到的,我们不得不在某种程度上妥协语法,原因如下:URL是一个字符串而不仅仅是一个自由格式的标识符。 这是因为我们没有macros-by-example

超级库更喜欢使用StatusCode枚举来获取HTTP返回码,所以我们只是在这里使用它。
这是运行该程序的输出:

在这里插入图片描述

花点时间思考一下使用宏的好处是什么。 这可以作为库调用实现,但宏也有一些好处,即使在这种基本形式。 一个是在编译时检查HTTP动词,因此您可以保证成功编译的程序不会尝试进行POST调用。 此外,我们能够将其作为一种迷你语言实现,其中=>标识符表示左侧命令与右侧预期返回值之间的分隔。

请注意,Rust需要读取第一个匹配($ url:tt)之后的宏输入,并且仅在空格后的第一个字母处(可以是GET的第一个字母或任何有效输入的POST的第一个字母)可以 继续只有一个模式匹配。

演习

1.编写一个宏,它接受任意数量的元素并输出一个无序的HTML列表
文字字符串。 例如,html_list!([1,2])=>

  • 1 /
  • 2 </ li> </ ul>。
    2.编写一个接受以下语言的宏:

language = HELLO recipient;
recipient = <String>;

例如,以下字符串在此语言中是可接受的:

HELLO world!
HELLO Rustaceans!

使宏生成代码,输出指向收件人的问候语。

3.编写一个宏,它接受这两个任意序列中的任何一个:

1, 2, 3
1 => 2; 2 => 3

对于第一个模式,它应该生成一个包含所有值的向量。 对于第二个模式,它应该生成具有键值对的HashMap。

概要

在本章中,我们简要介绍了元编程,并粗略地看了一下Rust支持的各种元编程并将支持。支持最多的形式是宏 - 示例,它完全适用于稳定的Rust。它们由macro_rules定义!宏。宏通过示例在抽象语法树级别工作,这意味着它们不支持任意扩展,但要求宏扩展在AST中格式良好。

我们研究了调试宏的方法,首先要求我们的编译器输出完全展开的形式( - 相当扩展)。通过宏log_syntax调试宏的第二种方法!和trace_macros !,需要夜间编译器,但是更加方便。

宏是一个强大的工具,但不应该轻易使用。只有当函数,特征和泛型等更稳定的机制不够用时,我们才能转向宏。

下一章将介绍更强大的过程宏技术。

猜你喜欢

转载自blog.csdn.net/m0_37696990/article/details/82903478
今日推荐