当前位置: 首页 > news >正文

Rust基础拾遗--辅助功能

Rust基础拾遗

  • 前言
  • 1.错误处理
    • 1.1 panic
    • 展开调用栈
    • 中止
    • Result
    • 捕捉错误
    • Result错误别名
    • 打印错误
    • 传播错误
    • 处理多种Error类型
    • 处理“不可能发生”的错误
    • 处理main() 中的错误
    • 声明自定义错误类型
    • 为什么是 Result
  • 2. create与模块
  • 3. 宏
    • 3.1 宏基础
      • 3.1.1 宏展开的基础
      • 3.1.2 意外后果
      • 3.1.3 重复
    • 3.2 内置宏
    • 3.3 调试宏
    • 3.4 构建json!宏
      • 3.4.1 片段类型
      • 3.4.2 宏中的递归
      • 3.4.4 作用域界定与卫生宏
      • 3.4.5 导入宏和导出宏
    • 3.5 在匹配过程中避免语法错误


前言

   通过Rust程序设计-第二版笔记的形式对Rust相关重点知识进行汇总,读者通读此系列文章就可以轻松的把该语言基础捡起来。


1.错误处理

Rust 中的两类错误处理:panic 和 Result。

  • 普通错误使用 Result 类型来处理。Result 通常用以表示由程序外部的事物引发的错误,比如错误的输入、网络中断或权限问题。
  • panic 针对的是另一种错误,即那种永远不应该发生的错误。

1.1 panic

当程序遇到下列问题的时候,就可以断定程序自身存在 bug,故而会引发 panic:

  • 数组越界访问;
  • 整数除以 0;
  • 在恰好为 Err 的 Result 上调用 .expect();
  • 断言失败。

panic!() 是一种宏,用于处理程序中出现错误的情况。

如果panic真的发生了,那么该怎么办呢?
Rust 为你提供了一种选择。Rust 既可以在发生 panic 时展开调用栈,也可以中止进程。

展开调用栈

panic 是安全的,没有违反 Rust 的任何安全规则,即使你故意在标准库方法的中间引发 panic,它也永远不会在内存中留下悬空指针或半初始化的值。Rust 的设计理念是要在出现任何意外之前捕获诸如无效数组访问之类的错误。继续往下执行显然是不安全的,所以 Rust 会展开这个调用栈。但是进程的其余部分可以继续运行。

panic 是基于线程的。一个线程 panic 时,其他线程可以继续做自己的事。

为了使程序更加健壮,可以使用线程和 catch_unwind() 来处理 panic。

中止

如果 Rust 在试图清理第一个 panic 时,.drop() 方法触发了第二个 panic,那么这个 panic 就是致命的。Rust 会停止展开调用栈并中止整个进程。

Result

Rust 中没有异常。相反,函数执行失败时会有像下面这样的返回类型:

fn get_weather(location: LatLng) -> Result<WeatherReport, io::Error>

Result 类型会指示出可能的失败。当我们调用 get_weather() 函数时,它要么返回一个成功结果 Ok(weather),其中的 weather 是一个新的 WeatherReport 值;要么返回一个错误结果 Err(error_value),其中的 error_value 是一个 io::Error,用来解释出了什么问题。

每当调用此函数时,Rust 都会要求我们编写某种错误处理代码。如果不对 Result 执行某些操作,就无法获取 WeatherReport;如果未使用 Result 值,就会收到编译器警告。

本章将采用类似“食谱”的方式并专注于使用 Result 来实现你期望的错误处理行为。你将了解如何捕获错误、传播错误和报告错误,以及关于组织和使用 Result 类型的常见模式。

捕捉错误

Result 最彻底的处理方式:使用 match 表达式。

match get_weather(hometown) {Ok(report) => {display_weather(hometown, &report);}Err(err) => {println!("error querying the weather: {}", err);schedule_weather_retry();}
}

这相当于其他语言中的 try/catch。如果想直接处理错误而不是将错误传给调用者,就可以使用这种方式。

match 有点儿冗长,因此 Result 针对一些常见的特定场景提供了多个有用的方法,每个方法在其实现中都有一个 match 表达式。

返回一个 bool,告知此结果是成功了还是出错了。result.ok()(成功值)以 Option 类型返回成功值(如果有的话)。如果 result 是成功的结果,就返回 Some(success_value);否则,返回 None,并丢弃错误值。result.err()(错误值)以 Option 类型返回错误值(如果有的话)。result.unwrap_or(fallback)(解包或回退值)

如果 result 为成功结果,就返回成功值;否则,返回 fallback,丢弃错误值。

// 对南加州而言,这是一则十拿九稳的天气预报
const THE_USUAL: WeatherReport = WeatherReport::Sunny(72);// 如果可能,就获取真实的天气预报;如果不行,就回退到常见状态
let report = get_weather(los_angeles).unwrap_or(THE_USUAL);
display_weather(los_angeles, &report);

这是 .ok() 的一个很好的替代方法,因为返回类型是 T,而不是 Option。当然,只有存在合适的回退值时,才能用这个方法。result.unwrap_or_else(fallback_fn)(解包,否则调用)

这个方法也一样,但不会直接传入回退值,而是传入一个函数或闭包。它针对的是大概率不会用到回退值且计算回退值会造成浪费的情况。只有在得到错误结果时才会调用 fallback_fn。

let report = get_weather(hometown) .unwrap_or_else(|_err| vague_prediction(hometown));

最后这两个方法之所以有用,是因为前面列出的所有其他方法,除了 .is_ok() 和 .is_err(),都在消耗 result。也就是说,它们会按值接受 self 参数。有时在不破坏 result 的情况下访问 result 中的数据是非常方便的,这就是 .as_ref() 和 .as_mut() 的用武之地。假设你想调用 result.ok(),但要让 result 保持不可变状态,那么就可以写成 result.as_ref().ok(),它只会借用 result,返回 Option<&T> 而非 Option。

Result错误别名

打印错误

传播错误

处理多种Error类型

处理“不可能发生”的错误

有时我们明确知道某个错误不可能发生。假设我们正在编写代码来解析配置文件,并且确信文件中接下来的内容肯定是一串数字:

if next_char.is_digit(10) {let start = current_index;current_index = skip_digits( &line, current_index);let digits = & line[start..current_index];...
}

我们想将这个数字串转换为实际的数值。有一个标准方法可以做到这一点:

let num = digits.parse::();

现在的问题是:str.parse:😦) 方法不返回 u64,而是返回了一个 Result。转换可能会失败,因为某些字符串不是数值:

"bleen".parse::() // ParseIntError: 无效的数字

但我们碰巧知道,在这种情况下,digits 一定完全由数字组成。那么应该怎么办呢?如果我们正在编写的代码已经返回了 GenericResult,那么就可以添加一个 ?,并且忽略这个错误。否则,我们将不得不为处理不可能发生的错误而烦恼。最好的选择是使用 Result 的 .unwrap() 方法。如果结果是 Err,就会 panic;但如果成功了,则会直接返回 Ok 中的成功值:

let num = digits.parse::().unwrap();

这和 ? 的用法很相似,但如果我们对这个错误有没有可能发生的理解是错误的,也就是说如果它其实有可能发生,那么这种情况就会报 panic。

事实上,对于刚才这个例子,我们确实理解错了。如果输入中包含足够长的数字串,则这个数值会因为太大而无法放入 u64 中:

"99999999999999999999".parse::() // 溢出错误

因此,在这种特殊情况下使用 .unwrap() 存在 bug。这种有 bug 的输入本不应该引发 panic。

话又说回来,确实会出现 Result 值不可能是错误的情况。例如,在第 18 章中,你会看到 Write 特型为文本和二进制输出定义了一组泛型方法(.write() 等)。所有这些方法都会返回 io::Result,但如果你碰巧正在写入 Vec,那么它们就不可能失败。在这种情况下,可以使用 .unwrap() 或 .expect(message) 来简化 Result 的处理。

当错误表明情况相当严重或异乎寻常,理当用 panic 对它进行处理时,这些方法也很有用:

fn print_file_age(filename: &Path, last_modified: SystemTime) {let age = last_modified.elapsed().expect("system clock drift");...
}

在这里,仅当系统时间早于文件创建时间时,.elapsed() 方法才会失败。如果文件是最近创建的,并且在程序运行期间系统时钟往回调整过,就会发生这种情况。根据这段代码的使用方式,在这种情况下,调用 panic 是一个合理的选择,而不必处理该错误或将该错误传播给调用者。

处理main() 中的错误

在大多数生成 Result 的地方,让错误冒泡到调用者通常是正确的行为。这就是为什么 ? 在 Rust 中会设计成单字符语法。正如我们所见,在某些程序中,它曾连续用于多行代码。但是,如果你传播错误的距离足够远,那么最终它就会抵达 main(),后者必须对其进行处理。通常来说,main() 不能使用 ?,因为它的返回类型不是 Result:处理 main() 中错误的最简单方式是使用 .expect():

fn main() {calculate_tides().expect("error"); // 责任止于此
}

如果 calculate_tides() 返回错误结果,那么 .expect() 方法就会 panic。主线程中的 panic 会打印出一条错误消息,然后以非零的退出码退出,大体上,这就是我们期望的行为。

声明自定义错误类型

编写一个新的 JSON 解析器,并且希望它有自己的错误类型。

// json/src/error.rs#[derive(Debug, Clone)]
pub struct JsonError {pub message: String,pub line: usize,pub column: usize,
}

这个结构体叫作 json::error::JsonError。当你想引发这种类型的错误时,可以像下面这样写:

return Err(JsonError {message: "expected ']' at end of array".to_string(),line: current_line,column: current_column
});

但是,如果你希望达到你的库用户的预期,确保这个错误类型像标准错误类型一样工作,那么还有一点儿额外的工作要做:

use std::fmt;// 错误应该是可打印的
impl fmt::Display for JsonError {fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {write!(f, "{} ({}:{})", self.message, self.line, self.column)}
}// 错误应该实现std::error::Error特型,但使用Error各个方法的默认定义就够了
impl std::error::Error for JsonError { }

与 Rust 语言的许多方面一样,各种 crate 的存在是为了让错误处理更容易、更简洁。crate 种类繁多,但最常用的一个是 thiserror,它会帮你完成之前的所有工作,让你像下面这样编写错误定义:

use thiserror::Error;#[derive(Error, Debug)]
#[error(" (, )")]
pub struct JsonError {message: String,line: usize,column: usize,
}

#[derive(Error)] 指令会让 thiserror 生成前面展示过的代码,这可以节省大量的时间和精力。

为什么是 Result

现在我们已经足够了解为何 Rust 会优先选择 Result 而非异常了。以下是此设计的几个要点。

  • Rust 要求程序员在每个可能发生错误的地方做出某种决策,并将其记录在代码中。这样做很好,否则容易因为疏忽而无法正确处理错误。

  • 最常见的决策是让错误继续传播,而这用单个字符 ? 就可以实现。因此,错误处理管道不会像在 C 和 Go 中那样让你的代码混乱不堪,而且它还具有可见性:在浏览一段代码时,你一眼就能看出错误是从哪里传出来的。

  • 是否可能出错是每个函数的返回类型的一部分,因此哪些函数会失败、哪些不会失败非常清晰。如果你将一个函数改为可能出错的,那么就要同时更改它的返回类型,而编译器会让你随之修改该函数的各个下游使用者。

  • Rust 会检查 Result 值是否被用过了,这样你就不会意外地让错误悄悄溜过去。

  • 由于 Result 是一种与任何其他数据类型没有本质区别的数据类型,因此很容易将成功结果和错误结果存储在同一个集合中,也很容易对“部分成功”的情况进行模拟。

2. create与模块

3. 宏

Rust 支持宏。宏是一种扩展语言的方式,它能做到单纯用函数无法做到的一些事。例如,我们已经见过 assert_eq! 宏,它是用于测试的好工具:

assert_eq!(gcd(6, 10), 2);

这也可以写成泛型函数,但是 assert_eq! 宏能做到一些无法用函数做到的事。一是当断言失败时,assert_eq! 会生成一条错误消息,其中包含断言的文件名和行号。函数无法获取这些信息,而宏可以,因为它们的工作方式完全不同。宏是一种简写形式。在编译期间,在检查类型并生成任何机器码之前,每个宏调用都会被展开。也就是说,每个宏调用都会被替换成一些 Rust 代码。前面的宏调用展开后大致如下所示:

3.1 宏基础

assert_eq! 宏的部分源代码。

在这里插入图片描述macro_rules! 是在 Rust 中定义宏的主要方式。请注意,这个宏定义中的 assert_eq 之后没有 !:只有调用宏时才要用到 !,定义宏时不用。

但并非所有的宏都是这样定义的:有一些宏是内置于编译器中的,比如 file!、line! 和 macro_rules!。本章会在结尾处讨论另一种方法,称为过程宏。但在本章的大部分内容里,我们会聚焦于 macro_rules!,这是迄今为止编写宏的最简单方式。

使用 macro_rules! 定义的宏完全借助“模式匹配”方式发挥作用。宏的主体只是一系列规则:

(pattern1)=>(template1);
(pattern2)=>(template3);
...

assert_eq! 版本只有一个模式和一个模板。

可以在模式或模板周围随意使用方括号或花括号来代替圆括号,这对 Rust 没有影响。同样,在调用宏时,下面这些都是等效的:

assert_eq!(gcd(6, 10), 2);
assert_eq![gcd(6, 10), 2];
assert_eq!{gcd(6, 10), 2}

唯一的区别是花括号后面的分号通常是可选的。按照惯例,在调用 assert_eq! 时使用圆括号,在调用 vec! 时使用方括号,而在调用 macro_rules! 时使用花括号。

3.1.1 宏展开的基础

Rust 在编译期间的很早阶段就展开了宏。编译器会从头到尾阅读你的源代码,定义并展开宏。你不能在定义宏之前就调用它,因为 Rust 在查看程序的其余部分之前就已经展开了每个宏调用。

Rust 展开 assert_eq! 宏调用的过程与对 match 表达式求值很像。Rust 会首先将参数与模式进行匹配,如图 21-2 所示。

在这里插入图片描述

宏模式是 Rust 中的一种迷你语言。它们本质上是用来匹配代码的正则表达式。不过正则表达式操作的是字符,而模式操作的是语法标记(Token,包括数值、名称、标点符号等),这些语法标记是 Rust 程序的基础构造块。这意味着可以在宏模式中自由使用注释和空白字符,以尽量提高模式的可读性。因为注释和空白字符不是语法标记,所以不会影响匹配。

正则表达式和宏模式之间的另一个重要区别是圆括号、方括号和花括号在 Rust 中总是成对出现。Rust 会在展开宏之前进行检查,不仅仅在宏模式中检查,而且会贯穿整个语言。

在此示例中,我们的模式包含片段 $left:expr,它告诉 Rust 要匹配一个表达式(在本例中是 gcd(6, 10))并将其命名为 $left。然后 Rust 会将模式中的逗号与 gcd 的参数后面的逗号进行匹配。就像正则表达式一样,模式中只有少数特殊字符会触发有意义的匹配行为;其他字符,比如逗号,则必须逐字匹配,否则匹配就会失败。最后,Rust 会匹配表达式 2 并将其命名为 $right。

这个模式中的两个代码片段都是 expr 类型的,表示它们期待表达式。21.4.1 节会展示其他类型的代码片段。因为这个模式已经匹配到了所有的参数,所以 Rust 展开了相应的模板,如图 21-3 所示。

在这里插入图片描述Rust 会将 $left 和 $right 替换为它在匹配过程中找到的代码片段。

在输出模板中包含片段类型(比如写成 $left:expr 而不仅是 $left)是一个常见的错误。Rust 不会立即检测到这种错误。它会将 $left 视为替代品,然后将 :expr 视为模板中的其他内容——要包含在宏输出中的语法标记。所以宏在被调用之前不会发生错误,然而它将生成实际无法编译的伪输出。如果在使用新宏时收到像 cannot find type ‘expr’ in this scope 和 help: maybe you meant to use a path separator here 这样的错误消息,请检查是否存在这种错误。

3.1.2 意外后果

将代码片段插入模板与用来处理值的常规代码略有不同。这些差异起初并不明显。我们一直在讲的宏 assert_eq! 就包含一些略显奇怪的代码,其原因大部分和宏编程有关。我们重点看看其中两个比较有意思的部分。首先,为什么这个宏会创建变量 left_val 和 right_val?为什么不能将模板简化成下面这样呢?

if !($left == $right) {
panic!(“assertion failed: (left == right)
(left: {:?}, right: {:?})”, $left, $right)
}

要回答这个问题,请尝试在心里展开宏调用 assert_eq!(letters.pop(), Some(‘z’))。它的输出会是什么呢?自然,Rust 会将匹配的表达式插入模板中的多个位置。但是,在构建错误消息时重新计算表达式似乎是个坏主意,不仅仅是因为需要花两倍的时间,更是因为这会导致第二次调用后它的值发生变化(因为 letters.pop() 会从向量中移除一个值)。这就是为什么真正的宏只会计算一次 $left 和 $right 并存储它们的值。继续第二个问题:为什么这个宏会借用对 $left 值和 $right 值的引用?为什么不像下面这样将值存储在变量中?

macro_rules! bad_assert_eq {
($left:expr, KaTeX parse error: Expected '}', got 'EOF' at end of input: … => ({ match (left, $right) {
(left_val, right_val) => {
if !(left_val == right_val) {
panic!(“assertion failed” /* … */);
}
}
}
});
}

对于我们一直在考虑的这个特定情况(宏参数是整数),这当然会正常工作。但是,如果调用者将一个 String 变量作为 $left 或 $right 传递,则上述代码会将该值移动出变量。

fn main() {
let s = “a rose”.to_string();
bad_assert_eq!(s, “a rose”);
println!(“confirmed: {} is a rose”, s); // 错误:使用了已移动出去的值 “s”
}

我们不希望断言移动值,因此这个宏改成了借入引用。(你可能想知道为什么这个宏要使用 match 而不是 let 来定义变量。嗯……我们也想知道。事实证明这样做没有特别的原因。使用 let 也可以达到同样的效果。)简而言之,宏可以做一些令人惊讶的事情。如果在你编写的宏周围发生了某些奇怪的事,那么很可能就是宏造成的。

你肯定不会看到下面这个经典的 C++ 宏 bug:// 有bug的C++宏:把数值n加上1#define ADD_ONE(n) n + 1由于大多数 C++ 程序员很熟悉,因而不值得在这里展开解释的原因,对于像 ADD_ONE(1) * 10 或 ADD_ONE(1 << 4) 这样不起眼的代码,使用这个宏会产生令人非常吃惊的结果。要修复这个 bug,就要在宏定义中添加更多圆括号。这在 Rust 中是不必要的,因为 Rust 宏能更好地与语言集成。Rust 知道自己什么时候是在处理表达式,因此在将一个表达式粘贴到另一个表达式时能有效地添加合理的圆括号。

3.1.3 重复

标准的 vec! 宏有两种形式:
// 把一个值重复N次
let buffer = vec![0_u8; 1000];

// 由逗号分隔的值列表
let numbers = vec![“udon”, “ramen”, “soba”];

它可以这样实现:

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

这里有 3 条规则。我们将解释“多规则”宏的工作原理,然后再依次查看每条规则。Rust 在展开像 vec![1, 2, 3] 这样的宏调用时,会先尝试将参数 1, 2, 3 与第一条规则的模式相匹配,在本例中就是 $elem:expr ; n : e x p r 。这无法匹配上,因为 1 是一个表达式,但模式要求其后有一个分号,而这里没有。所以 R u s t 继续匹配第二条规则,以此类推。如果没有匹配任何规则,则视为错误。第一条规则处理像 v e c ! [ 0 u 8 ; 1000 ] 这样的用法。碰巧标准库(但未写入文档)函数 s t d : : v e c : : f r o m e l e m 完全满足这里的需要,所以这条规则是显而易见的。第二条规则处理 v e c ! [ " u d o n " , " r a m e n " , " s o b a " ] 。 n:expr。这无法匹配上,因为 1 是一个表达式,但模式要求其后有一个分号,而这里没有。所以 Rust 继续匹配第二条规则,以此类推。如果没有匹配任何规则,则视为错误。第一条规则处理像 vec![0u8; 1000] 这样的用法。碰巧标准库(但未写入文档)函数 std::vec::from_elem 完全满足这里的需要,所以这条规则是显而易见的。第二条规则处理 vec!["udon", "ramen", "soba"]。 n:expr。这无法匹配上,因为1是一个表达式,但模式要求其后有一个分号,而这里没有。所以Rust继续匹配第二条规则,以此类推。如果没有匹配任何规则,则视为错误。第一条规则处理像vec![0u8;1000]这样的用法。碰巧标准库(但未写入文档)函数std::vec::fromelem完全满足这里的需要,所以这条规则是显而易见的。第二条规则处理vec!["udon","ramen","soba"]( $x:expr ),* 模式使用了我们从未见过的一个特性:重复。它会匹配 0 个或多个表达式,以逗号分隔。更一般地说,语法 $( PATTERN ),* 可用于匹配任何以逗号分隔的列表,其中列表的每个条目都会匹配 PATTERN。这里的 * 与正则表达式中的 * 具有相同的含义(“0 或更多”),只是公认的正则表达式中并没有特殊的 ,* 重复器。还可以使用 + 要求至少匹配一次,或者使用 ? 要求有 0 个或 1 个匹配项。表 21-1 给出了全套的重复模式。

代码片段 $x 不是单个表达式,而是一个表达式列表。这条规则的模板也使用了重复语法:

<[_]>::into_vec(Box::new([ $( $x ),* ]))

同样,有一些标准库方法可以完全满足我们的需要。此代码会创建一个 Box 数组,然后使用 [T]::into_vec 方法将 Box 数组转换为向量。第一个代码片段 <[]> 是用于编写“某物的切片”类型的一种不寻常的方式,它会期待 Rust 推断出元素类型。那些名称为普通标识符的类型可以不经任何修改直接用在表达式中,但是像 fn()、&str 或 [] 这样的特殊类型必须用尖括号括起来。重复要出现在模板的末尾,在我们这里是 ( ( (x),。这个 $(…), 与我们在模式中看到的语法是一样的。它会遍历我们为 $x 匹配出的表达式列表,并将它们全部插入模板中,以逗号分隔。在这个例子中,重复输出看起来和重复输入差不多。但事实并非如此。也可以这样写规则:

( $( $x:expr ),* ) => {
{
let mut v = Vec::new();
$( v.push($x); )*
v
}
};

在这里,模板中读取 ( v . p u s h ( ( v.push( (v.push(x); )* 的部分会为 $x 中的每个表达式插入对 v.push() 的调用。宏里的分支可以展开为一系列表达式,但这里只需要一个表达式,所以要把向量的集合包装在一个块中。与 Rust 的其余部分不同,使用 $( … ),* 的模式不会自动支持可选的尾随逗号。但是,有一个标准技巧,即通过添加额外的规则来支持尾随逗号。这正是 vec! 宏的第三条规则的作用:

( $( $x:expr ),+ ,) => { // 如果出现了尾随逗号,
vec![ $( $x ),* ] // 就按没有这个逗号时的样子重试
};

我们使用 $( … ),+ , 来匹配带有额外逗号的列表。然后在模板中递归地调用 vec!,并剥离额外的逗号。这次会匹配上第二条规则。

3.2 内置宏

Rust 编译器提供了几个内置宏,它们在你定义自己的宏时很有用。这些宏都不能使用 macro_rules! 来实现。它们是硬编码在 rustc 中的。

file!()(文件名)、line!()(行号)和 column!()(列号)

file!() 会展开为字符串字面量,即当前文件名。line!() 和 column!() 会展开为 u32 字面量,以给出当前行号和列号(从 1 开始计数)。如果一个宏调用了另一个宏,后者又调用了别的宏,并且最后一个宏调用了 file!()、line!() 或 column!(),而它们都在不同的文件中,则最终展开之后的结果指示的是第一个宏所在的位置。

stringify!(…tokens…)(代码字符串)

该宏会展开为包含给定语法标记的字符串字面量。assert! 宏会使用它来生成包含断言代码的错误消息。它的参数中的宏调用不会展开,比如 stringify!(line!()) 会展开为字符串 “line!()”。Rust 会从这些语法标记构造出字符串,因此字符串中没有换行符或注释(因为它们都不是语法标记)。

concat!(str0, str1, …)(串联)

该宏会通过串联它的各个参数展开为单个字符串字面量。Rust 还定义了下面这些用于查询构建环境的宏。

cfg!(…)(配置)

该宏会展开为布尔常量,如果当前正构建的配置与圆括号中的条件匹配则为 true。如果在启用了调试断言的情况下进行编译,则 cfg!(debug_assertions) 为 true。这个宏支持与 8.5 节所讲的 #[cfg(…)] 属性完全相同的语法,但其结果不是条件编译,而是给出像 true 或 false 这样的答案。

展开为字符串,即在编译期指定的环境变量的值。如果该变量不存在,则为编译错误。Cargo 会在编译 crate 时设置几个有实质性内容的环境变量,这样此宏才有价值。例如,要获取 crate 的当前版本字符串,可以这样写:

let version = env!(“CARGO_PKG_VERSION”);

Cargo 文档中有这些环境变量的完整列表。option_env!(“VAR_NAME”)(可选环境变量)和 env! 基本一样,不过该宏会返回 Option<&'static str>,如果没有设置指定的变量,则返回 None。以下 3 个内置宏支持从另一个文件中引入代码或数据。include!(“file.rs”)(包含代码文件)该宏会展开为指定文件的内容,这个文件必须是有效的 Rust 代码——表达式或语法项的序列。include_str!(“file.txt”)(包含字符串)该宏会展开为包含指定文件中文本的 &'static str。可以像这样使用它:

const COMPOSITOR_SHADER: &str =
include_str!(“…/resources/compositor.glsl”);

如果文件不存在或不是有效的 UTF-8 格式,你将收到编译错误。include_bytes!(“file.dat”)(包含一些字节)和上一个宏基本相同,不过该宏会把文件视为二进制数据而非 UTF-8 文本。结果是 &'static [u8]。与所有宏一样,这些内置宏也都在编译期处理。如果文件不存在或无法读取,则编译失败。它们不会在运行期失败。在任何情况下,如果文件名是相对路径,就会相对于当前文件所在的目录进行解析。Rust 还提供了几个我们未曾涉及的便捷宏。todo!()(待做)和 unimplemented!()(未实现)这两个宏的作用相当于 panic!(),但传达了不同的意图。unimplemented!() 适用于 if 子句、match 分支和其他尚未处理的情况。它总会 panic。todo!() 大致相同,但传达了这样的想法,即这段代码还没有编写,一些 IDE 会对其进行标记以提请关注。matches!(value, pattern)(匹配)将值与模式进行比较,如果匹配就返回 true,否则返回 false。类似于如下写法:match value { pattern => true, _ => false}如果你正在寻求编写基本宏的练习,那么该宏是一个很好的复刻目标——特别是你可以直接在标准库文档中找到它的源代码,其实现非常简单。

3.3 调试宏

调试率性而为的宏颇具挑战性。最大的问题是宏展开过程缺乏可见性。Rust 经常会展开所有宏,在发现某种错误后打印一条错误消息,但不会展示包含该错误的完全展开后的代码。以下是 3 个有助于解决宏问题的工具。(这些特性都还不稳定,但由于它们实际上是为了在开发过程中使用而设计的,而不会出现在要签入的代码中,因此这在实践中并不是什么大问题。)第一,也是最简单的,可以让 rustc 展示代码在展开所有宏后的样子。使用 cargo build --verbose 查看 Cargo 如何调用 rustc。复制 rustc 命令行并添加 -Z unstable-options --pretty expanded 选项。完全展开的代码将转储到终端。很遗憾,只有当代码没有语法错误时才能这样做。第二,Rust 提供了一个 log_syntax!() 宏,它只会在编译期将自己的参数打印到终端。你可以将其用于 println! 式调试。此宏需要添加 #![feature(log_syntax)] 特性标志。第三,可以要求 Rust 编译器把所有宏调用记录到终端。在代码中的某个地方插入 trace_macros!(true);。从那时起,每当 Rust 展开宏时,它都会打印宏的名称和各个参数。例如,考虑下面这个程序:#![feature(trace_macros)]fn main() { trace_macros!(true); let numbers = vec![1, 2, 3]; trace_macros!(false); println!(“total: {}”, numbers.iter().sum:😦));}它会生成这样的输出:$ rustup override set nightly…$ rustc trace_example.rsnote: trace_macro --> trace_example.rs:5:19 |5 | let numbers = vec![1, 2, 3]; | ^^^^^^^^^^^^^ | = note: expanding vec! { 1 , 2 , 3 } = note: to < [ _ ] > :: into_vec ( box [ 1 , 2 , 3 ] )编译器会展示每个宏调用的代码,既包括展开前的也包括展开后的。

3.4 构建json!宏

我们已经讨论过了 macro_rules! 的核心特性。本节将逐步开发用于构建 JSON 数据的宏。我们将使用这个例子来展示宏的开发过程,同时会介绍 macro_rules! 剩下的几个特性,并就如何确保你的宏如预期般运行提供一些建议。

JSON 数据的枚举:
#[derive(Clone, PartialEq, Debug)]
enum Json {
Null,
Boolean(bool),
Number(f64),
String(String),
Array(Vec),
Object(Box<HashMap<String, Json>>)
}
我们希望使用更具 JSON 风格的语法来编写:

let students = json!([{"name": "Jim Blandy","class_of": 1926,"major": "Tibetan throat singing"},{"name": "Jason Orendorff","class_of": 1702,"major": "Knots"}
]);

我们想要的是一个将 JSON 值作为参数并展开为 Rust 表达式的 json! 宏。

3.4.1 片段类型

如果想编写一个复杂的宏,那么第一项工作是弄清楚如何匹配或解析所期望的输入。我们可以预见 Json 宏内部将会有多条规则,因为 JSON 数据有多种类型:对象、数组、数值等。事实上,我们可以合理地猜测每种 JSON 类型都将有一条规则:

macro_rules! json {(null) => { Json::Null };([ ... ]) => { Json::Array(...) };({ ... }) => { Json::Object(...) };(???) => { Json::Boolean(...) };(???) => { Json::Number(...) };(???) => { Json::String(...) };
}

3.4.2 宏中的递归

我们已经看过宏调用自身的一个简单例子:vec! 的实现就使用了递归来支持尾随逗号。这里我们会展示一个更重要的例子:json! 需要递归调用自己。

我们可能会尝试在不使用递归的情况下支持 JSON 数组,如下所示:

([ $( $element:tt ),* ]) => {Json::Array(vec![ $( $element ),* ])
};

但这是行不通的,因为这样做就会将 JSON 数据($element 语法标记树)直接粘贴到 Rust 表达式中,而它们是两种不同的语言。

因此,我们需要将数组的每个元素从 JSON 格式转换为 Rust。幸运的是,有一个宏可以执行此操作,也就是我们正在写的这个。

([ $( $element:tt ),* ]) => {Json::Array(vec![ $( json!($element) ),* ])
};

可以用相同的方式支持对象:

({ $( $key:tt : $value:tt ),* }) => {Json::Object(Box::new(vec![$( ($key.to_string(), json!($value)) ),*].into_iter().collect()))
};

编译器会对宏施加递归限制:默认情况下最多递归 64 层。这对于 json! 这样的正常用法足够了,但复杂的递归宏有时会达到这个极限。可以通过在使用宏的 crate 顶部添加如下属性来调整它:

#![recursion_limit = "256"]

我们的 json! 宏接近完成了。剩下的就是支持布尔值、数值和字符串值。

3.4.4 作用域界定与卫生宏

编写宏时需要将来自不同作用域的代码粘贴在一起。

Rust 处理作用域的两种方式:一种方式用于局部变量和参数,另一种方式用于其他一切。

我们来重写一下解析 JSON 对象的规则(前面展示的 json! 宏中的第三条规则)以消除临时向量。

代码示例:

({ $($key:tt : $value:tt),* }) => {{let mut fields = Box::new(HashMap::new());$( fields.insert($key.to_string(), json!($value)); )*Json::Object(fields)}
};

现在不是通过使用 collect() 而是通过重复调用 .insert() 方法来填充 HashMap。这意味着需要将此映射表存储在名为 fields 的临时变量中。但是如果调用 json! 时碰巧使用了自己的一个变量,而这个变量也叫 fields,会发生什么呢?

let fields = "Fields, W.C.";
let role = json!({"name": "Larson E. Whipsnade","actor": fields
});

展开宏会将两小段代码粘贴在一起,两者都使用 fields 这个名字,但表示不同的东西。

let fields = "Fields, W.C.";
let role = {let mut fields = Box::new(HashMap::new());fields.insert("name".to_string(), Json::from("Larson E. Whipsnade"));fields.insert("actor".to_string(), Json::from(fields));Json::Object(fields)
};

每当宏使用临时变量时,这似乎是一个无法回避的陷阱。

不用担心,Rust 会替你重命名此变量。这个特性是首先在 Scheme 语言的宏中实现的,被称为卫生的(hygiene),因此 Rust 被称为支持卫生宏(hygienic macro)的语言。理解卫生宏的最简单方法是想象每次展开宏时,来自宏本身的展开结果都会被涂上不同的颜色。然后,不同颜色的变量被视为具有不同的名称:
在这里插入图片描述
请注意,由宏调用者传入并粘贴到输出中的那点儿代码(如 “name” 和 “actor”)会保持其原始颜色(黑色)。这里只会对源自宏模板的语法标记进行染色。

现在有一个名为 fields 的变量(在调用者中声明)和另一个同样名为 的变量(由宏引入)。由于名称是不同的颜色,因此这两个变量不会混淆。

如果宏确实需要引用调用者作用域内的变量,则调用者必须将变量的名称传给宏。(染色的比喻并不是要准确描述卫生宏的工作原理。真正的机制甚至比这种方式更“聪明一点儿”。只要两个标识符引用的是位于宏及其调用者作用域内的公共变量,不管“染成了什么颜色”,都能识别出它们是相同的。但这种情况在 Rust 中很少见。如果你理解前面的例子,就知道该如何使用卫生宏了。)

你可能已经注意到,随着宏的展开,许多其他标识符(比如 Box、HashMap 和 Json)被染上了不止一种颜色。虽然这些类型名称的颜色不同,但 Rust 仍然毫不费力地识别出了它们。那是因为 Rust 中的卫生工作仅限于局部变量和参数。对于常量、类型、方法、模块、静态值和宏名称,Rust 是“色盲”。

这意味着如果我们的 json! 宏在尚未导入 Box、HashMap 或 Json 的模块中使用,那么宏就无法正常工作。21.4.5 节会展示如何避免这一问题。首先,需要考虑 Rust 的严格卫生机制构成某种障碍的情况,我们要解决这一问题。假设我们有很多包含下面这行代码的函数:

let req = ServerRequest::new(server_socket.session());

解决方案是把你打算同时在宏代码内部和外部使用的任何标识符都传给宏:

macro_rules! setup_req {() => {let req = ServerRequest::new(server_socket.session());}
}fn handle_http_request(server_socket: &ServerSocket) {setup_req!(); // 使用`server_socket`来声明`req`…… // 使用`req`的代码
}

由于函数现在提供了 req 和 server_socket,因此它们在该作用域内就有正确的“颜色”了。

3.4.5 导入宏和导出宏

要将宏从当前模块“向上”导出到其父模块,请使用 #[macro_use] 属性。
例如 lib.rs :

#[macro_use] mod macros;
mod client;
mod server;

那么 macros 模块中定义的所有宏都会导入 lib.rs 中,因此在 crate 的其余部分(包括在 client 和 server 中)都可见。标有 #[macro_export] 的宏会自动成为公共的,并且可以像其他语法项一样通过路径引用。例如,lazy_static crate 提供了一个名为 lazy_static 的宏,该宏会被标记为 #[macro_export]。要在自己的 crate 中使用这个宏,可以这样写:

use lazy_static::lazy_static;
lazy_static!{ }

一旦导入了宏,就可以像其他任何语法项一样使用它:

use lazy_static::lazy_static;mod m {crate::lazy_static! { }
}

当然,实际上做这些就意味着你的宏可能会在其他模块中调用。因此,导出的宏不应该依赖作用域内的任何内容,因为在使用时无从确定它的作用域内会有哪些内容。它甚至可以遮蔽标准库预导入中的特性。相反,宏应该对它用到的任何名称都使用绝对路径。macro_rules! 提供了特殊片段 c r a t e 来帮助解决这个问题。这与 c r a t e 不同, c r a t e 关键字可以用在任何地方的路径中而不仅仅是宏中。 crate 来帮助解决这个问题。这与crate 不同,crate 关键字可以用在任何地方的路径中而不仅仅是宏中。 crate来帮助解决这个问题。这与crate不同,crate关键字可以用在任何地方的路径中而不仅仅是宏中。crate 相当于定义此宏的 crate 的根模块绝对路径。我们可以写成 $crate::Json 而非 Json 的形式,这样即使没有导入 Json 也能工作。HashMap 可以被更改为 ::std::collections::HashMap 或 $crate::macros::HashMap。在后一种情况下,我们将不得不重新导出 HashMap,因为 $crate 不能用于访问 crate 的私有特性。它实际上只是展开成类似于 ::jsonlib 的普通路径。可见性规则不受影响。将宏移到它自己的模块 macros 并修改为使用 $crate 后,代码是下面这样的。这是最终版本:

// macros.rs
pub use std::collections::HashMap;
pub use std::boxed::Box;
pub use std::string::ToString;#[macro_export]
macro_rules! json {(null) => {$crate::Json::Null};([ $( $element:tt ),* ]) => {$crate::Json::Array(vec![ $( json!($element) ),* ])};({ $( $key:tt : $value:tt ),* }) => {{let mut fields = $crate::macros::Box::new($crate::macros::HashMap::new());$(fields.insert($crate::macros::ToString::to_string($key),json!($value));)*$crate::Json::Object(fields)}};($other:tt) => {$crate::Json::from($other)};
}

由于 .to_string() 方法是标准 ToString 特型的一部分,因此我们也会通过 $crate 来引用它,而使用的语法是 11.3 节介绍的 c r a t e : : m a c r o s : : T o S t r i n g : : t o s t r i n g ( crate::macros::ToString::to_string( crate::macros::ToString::tostring(key)。在这个例子中,这一句并不是必要的,因为 ToString 位于标准库预导入中。但是,如果你调用的特型方法可能不在调用宏的作用域内,则使用完全限定的方法调用是最好的办法。

3.5 在匹配过程中避免语法错误

下面的宏看起来很合理,但它给 Rust 带来了一些麻烦:

macro_rules! complain {($msg:expr) => {println!("Complaint filed: {}", $msg)};(user : $userid:tt , $msg:expr) => {println!("Complaint from user {}: {}", $userid, $msg)};
}

假设我们这样调用它:

complain!(user: "jimb", "the AI lab's chatbots keep picking on me");

在人类眼中,这显然符合第二条规则。但是 Rust 会首先尝试第一条规则,试图将所有输入与 $msg:expr 匹配。事情开始变得棘手了。user: “jimb” 当然不是表达式,所以我们得到了一个语法错误。Rust 拒绝隐藏语法错误,因为调试宏本来就已经够艰难了,再隐藏语法错误简直要命。因此,它会立即报告并停止编译。

如果无法匹配模式中的任何其他语法标记,Rust 就会继续执行下一条规则。只有语法错误才会导致匹配失败,并且只在试图匹配片段时才会发生。

这里的问题并不难理解:我们是在错误的规则中试图匹配片段 $msg:expr。这无法匹配,因为我们本来就不应该在这里匹配。调用者想要匹配其他规则。有两种简单的方法可以避免这种情况。

第一种方法,避免混淆各条规则。例如,可以更改宏,让每个模式都以不同的标识符开头:

macro_rules! complain {(msg : $msg:expr) => {println!("Complaint filed: {}", $msg);};(user : $userid:tt , msg : $msg:expr) => {println!("Complaint from user {}: {}", $userid, $msg);};
}

当宏参数以 msg 开头时,就匹配第一条规则。当宏参数以 user 开头时,就匹配第二条规则。无论是哪种参数,我们都能知道在尝试匹配片段之前已经找到了正确的规则。

避免虚假语法错误的另一种方法是将更具体的规则放在前面。将 user: 规则放在前面就可以解决 complain! 的问题,因为永远不会到达导致语法错误的那条规则。

相关文章:

  • 惠普打印机驱动安装
  • 项目第一次git commit后如何撤销
  • JS进阶——垃圾回收机制以及算法
  • 【数学建模】【2024年】【第40届】【MCM/ICM】【B题 搜寻潜水器】【解题思路】
  • Rust 学习笔记 - 变量声明与使用
  • 用C语言列出Linux或Unix上的网络适配器
  • 5.9 BCC工具之nodejs_http_server.py简介
  • [前端开发] 常见的 HTML CSS JavaScript 事件
  • aiofiles:解锁异步文件操作的神器
  • Unity类银河恶魔城学习记录7-6 P72 Bouncy sword源代码
  • LLM之LangChain(七)| 使用LangChain,LangSmith实现Prompt工程ToT
  • 树与二叉树---数据结构
  • bat脚本 创建计划任务 一分钟设置ntp同步周期为60s
  • Gin框架: 快速搭建起一个Web应用环境及处理不同类型的响应
  • wordpress外贸成品网站模板
  • 【347天】每日项目总结系列085(2018.01.18)
  • 【5+】跨webview多页面 触发事件(二)
  • 2017届校招提前批面试回顾
  • conda常用的命令
  • express + mock 让前后台并行开发
  • PHP 使用 Swoole - TaskWorker 实现异步操作 Mysql
  • 大型网站性能监测、分析与优化常见问题QA
  • 工作手记之html2canvas使用概述
  • 观察者模式实现非直接耦合
  • 前端之Sass/Scss实战笔记
  • 实战:基于Spring Boot快速开发RESTful风格API接口
  • 数组大概知多少
  • 微信端页面使用-webkit-box和绝对定位时,元素上移的问题
  • 物联网链路协议
  • 用Node EJS写一个爬虫脚本每天定时给心爱的她发一封暖心邮件
  • 自动记录MySQL慢查询快照脚本
  • 看到一个关于网页设计的文章分享过来!大家看看!
  • 【运维趟坑回忆录】vpc迁移 - 吃螃蟹之路
  • python最赚钱的4个方向,你最心动的是哪个?
  • ​flutter 代码混淆
  • ## 临床数据 两两比较 加显著性boxplot加显著性
  • ###项目技术发展史
  • #大学#套接字
  • #每日一题合集#牛客JZ23-JZ33
  • $ is not function   和JQUERY 命名 冲突的解说 Jquer问题 (
  • (C语言版)链表(三)——实现双向链表创建、删除、插入、释放内存等简单操作...
  • (html5)在移动端input输入搜索项后 输入法下面为什么不想百度那样出现前往? 而我的出现的是换行...
  • (动手学习深度学习)第13章 计算机视觉---微调
  • (九)c52学习之旅-定时器
  • (理论篇)httpmoudle和httphandler一览
  • (转)MVC3 类型“System.Web.Mvc.ModelClientValidationRule”同时存在
  • .360、.halo勒索病毒的最新威胁:如何恢复您的数据?
  • .NET CLR Hosting 简介
  • .NET CORE Aws S3 使用
  • .net 设置默认首页
  • /bin、/sbin、/usr/bin、/usr/sbin
  • [.NET 即时通信SignalR] 认识SignalR (一)
  • [@Controller]4 详解@ModelAttribute
  • [2018/11/18] Java数据结构(2) 简单排序 冒泡排序 选择排序 插入排序
  • [ajaxupload] - 上传文件同时附件参数值