软件编写的很大一部分围绕着检查某些数据是否具有某种形状(“模式”),从中提取信息,然后在匹配时做出反应。为了促进这一点,许多现代语言(包括 Rust)都支持所谓的“模式匹配”。
如果您是 Rust 新手或想复习您的知识,您可能首先需要阅读本书中的第 6 章,枚举和模式匹配和第 18 章,模式和匹配,或者阅读更多关于参考手册中的
match
表达式和模式的内容。
Rust 中的模式匹配通过检查内存中的位置(“数据”)是否与特定模式匹配来工作。在本文中,我们将介绍一些最近在稳定 Rust 中即将推出的模式改进以及一些已在 nightly 版本中提供的模式改进。
如果您熟悉所讨论的 nightly 功能,并希望帮助推动它们走向稳定,请跳转到 我如何提供帮助?。
[head, tail @ ..]
子切片模式,列表是软件中最基本和最常见的数据结构之一。在 Rust 中,列表通常是内存中元素的连续序列,或一个切片。
由于切片非常普遍,因此轻松使用它们非常重要。为此,我们在Rust 1.26.0 中稳定了固定长度的切片模式。所以现在可以例如写 let [a, b, c] = my_array;
来解构一个包含 3 个元素的数组。然而,通常我们处理的是未知长度的切片,因此仅给定固定长度的切片模式,我们必须提供一个回退 match
分支,例如模式为 _
。
在 Rust 1.42.0 中,我们正在稳定子切片模式。为了引入子切片模式,我们使用 ..
,它表示可变长度的间隙,匹配尽可能多的未被 ..
前后模式匹配的元素。例如,在解析器中,当属性列表 attrs
后面没有跟项目时,我们希望出错,所以我们写道
/// Recover if we parsed attributes and expected an item but there was none.
fn recover_attrs_no_item(&mut self, attrs: &[Attribute]) -> PResult<'a, ()> {
let (start, end) = match attrs {
[] => return Ok(()),
[x0] => (x0, x0),
[x0, .., xn] => (x0, xn),
};
let msg = if end.is_doc_comment() {
"expected item after doc comment"
} else {
"expected item after attributes"
};
let mut err = self.struct_span_err(end.span, msg);
if end.is_doc_comment() {
err.span_label(end.span, "this doc comment doesn't document anything");
}
if let [.., penultimate, _] = attrs {
err.span_label(start.span.to(penultimate.span), "other attributes here");
}
Err(err)
}
这里我们有两个子切片模式,第一个是 [x0, .., xn]
。在这种情况下,模式绑定 x0
(第一个元素)和 xn
(最后一个元素),并忽略中间的所有内容,匹配总共至少有两个元素的切片。同时,[]
和 [x0]
匹配少于两个元素的案例,因此编译器知道我们已经涵盖了所有可能性。在后一种情况下,我们提取切片的 倒数第二个
元素,顾名思义,这也要求切片至少有两个元素。
我们还可以将子切片绑定到变量。例如,假设我们想要禁止在函数的所有参数(最后一个参数除外)中使用 ...
。如果是这样,我们可以写
match &*fn_decl.inputs {
... // other arms
[ps @ .., _] => {
for Param { ty, span, .. } in ps {
if let TyKind::CVarArgs = ty.kind {
self.err_handler().span_err(
*span,
"`...` must be the last argument of a C-variadic function",
);
}
}
}
}
这里,ps @ ..
将切片的初始元素绑定到 ps
并忽略最后一个元素。
在 nightly 中经过 7 年多的酝酿,经历了许多曲折,子切片模式最终将稳定下来。为了到达这里,我们不得不重新设计该功能,堵塞借用检查器中的健全性漏洞,并大幅重构详尽性检查器。有关我们如何到达这里的更多信息,请阅读稳定报告,Thomas Hartmann 的博客文章,并请关注 3 月 12 日的 1.42.0 发布公告。
嵌套 OR 模式
当对 enum
进行模式匹配时,某些变体的逻辑可能完全相同。为了避免重复自己,match
、if let
或 while let
表达式中的 |
分隔符可用于表示如果任何 |
分隔的模式匹配,则应采用该分支。例如,我们可以写
// Any local node that may call something in its body block should be explored.
fn should_explore(tcx: TyCtxt<'_>, hir_id: hir::HirId) -> bool {
match tcx.hir().find(hir_id) {
Some(Node::Item(..))
| Some(Node::ImplItem(..))
| Some(Node::ForeignItem(..))
| Some(Node::TraitItem(..))
| Some(Node::Variant(..))
| Some(Node::AnonConst(..))
| Some(Node::Pat(..)) => true,
_ => false,
}
}
这很有用,但是 Some(_)
仍然重复了好几次。使用 #![feature(or_patterns)]
(最近在 nightly 版本中变得可用),可以避免这种重复
// Any local node that may call something in its body block should be explored.
fn should_explore(tcx: TyCtxt<'_>, hir_id: hir::HirId) -> bool {
match tcx.hir().find(hir_id) {
Some(
Node::Item(..)
| Node::ImplItem(..)
| Node::ForeignItem(..)
| Node::TraitItem(..)
| Node::Variant(..)
| Node::AnonConst(..)
| Node::Pat(..),
) => true,
_ => false,
}
}
以前,在 match
表达式中使用 |
时,|
语法是 match
本身的一部分。使用 or_patterns
,现在这是模式本身的一部分,因此您可以任意嵌套 OR 模式,并在 let
语句中使用它们
let Ok(x) | Err(x) = foo();
OR 模式涵盖所有 |
-ed(“或”)模式的并集。为了确保无论哪个替代方案匹配,所有绑定都一致且已初始化,每个或模式都必须包含完全相同的一组绑定,具有相同的类型和相同的绑定模式。
@
之后的绑定
当匹配某个子结构时,有时您想保留整个结构。例如,给定 Some(Expr { .. })
,您想绑定外部 Some(_)
层。在 Rust 中,这可以使用例如 expr @ Some(Expr { .. })
来完成,它将匹配的位置绑定到 expr
,同时还确保它与 Some(Expr { .. })
匹配。
还假设 Expr
有一个字段 span
,您也会使用它。在远古时代,也就是 Rust 1.0 之前,这是可能的,但是今天,它会导致错误
error[E0303]: pattern bindings are not allowed after an `@`
--> src/lib.rs:L:C
|
L | bar @ Some(Expr { span }) => {}
| ^^^^ not allowed after `@`
这在 #16053 中变成了一个错误,主要是因为在旧的基于 AST 的借用检查器中以健全的方式编码借用检查规则的困难。
从那时起,我们删除了旧的借用检查器,转而使用基于 MIR 的检查器,MIR 是一个更简单、更适合借用检查的数据结构。具体来说,在诸如 let ref x @ ref y = a;
之类的语句中,我们将获得与使用 let x = &a; let y = &a;
时大致相同的 MIR。
因此,现在 @
右侧的绑定由借用检查器以统一且正确的方式处理(例如,编译器不允许 ref x @ ref mut y
),我们已决定在 #![feature(bindings_after_at)]
下允许它们,现在在 nightly 版本中可用。启用此功能后,您可以例如写
#![feature(bindings_after_at)]
fn main() {
if let x @ Some(y) = Some(0) {
dbg!(x, y);
}
}
我们希望通过提供此功能,消除该语言的一个令人惊讶的角落。
ref
绑定
组合按移动绑定和按 出于与 @
之后的绑定情况中提到的类似原因,Rust 目前不允许您将正常的按移动绑定与按 ref
的绑定组合在一起。例如,如果您写...
fn main() {
let tup = ("foo".to_string(), 0);
let (x, ref y) = tup;
}
...您会收到一个错误
error[E0009]: cannot bind by-move and by-ref in the same pattern
--> src/main.rs:3:10
|
3 | let (x, ref y) = tup;
| ^ ----- by-ref pattern here
| |
| by-move pattern here
同时,编译器非常乐意允许...
fn main() {
let tup = ("foo".to_string(), 0);
let x = tup.0;
let ref y = tup.1;
}
...即使这些程序之间没有语义差异。
现在我们已经迁移到新的借用检查器,如上一节所述,我们也放宽了 nightly 版本的限制,因此在 #![feature(move_ref_pattern)]
下,您可以写
#![feature(move_ref_pattern)]
fn main() {
let tup = ("foo".to_string(), 0);
let (x, ref y) = tup;
}
我如何提供帮助?
总而言之,我们有三个不稳定的功能,它们都以不同的方式改进了模式匹配
#![feature(or_patterns)]
,允许您任意嵌套 or 模式,例如Some(Foo | Bar)
#![feature(bindings_after_at)]
,允许例如ref x @ Some(ref y)
#![feature(move_ref_pattern)]
,允许例如(x, ref y)
,其中x
按移动绑定,y
按引用绑定
为了帮助我们将这些功能过渡到稳定的 Rust,我们需要您的帮助,以确保它们符合预期的质量标准。为了提供帮助,请考虑
- 在您的代码中尽可能使用这些功能(如果使用 nightly 编译器是您可以接受的),并报告任何错误、问题、诊断缺陷等作为问题。
- 查看功能门标签下报告的问题(例如
F-or_patterns
),看看您是否可以帮助解决其中任何问题。- 特别是,如果您可以帮助编写测试,我们将不胜感激。
感谢您的阅读,并祝您模式匹配愉快!