最近和未来模式匹配的改进

2020 年 3 月 4 日 · Mazdak "Centril" Farrokhzad 代表语言团队

软件编写的很大一部分围绕着检查某些数据是否具有某种形状(“模式”),从中提取信息,然后在匹配时做出反应。为了促进这一点,许多现代语言(包括 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 进行模式匹配时,某些变体的逻辑可能完全相同。为了避免重复自己,matchif letwhile 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;
}

我如何提供帮助?

总而言之,我们有三个不稳定的功能,它们都以不同的方式改进了模式匹配

为了帮助我们将这些功能过渡到稳定的 Rust,我们需要您的帮助,以确保它们符合预期的质量标准。为了提供帮助,请考虑

  • 在您的代码中尽可能使用这些功能(如果使用 nightly 编译器是您可以接受的),并报告任何错误、问题、诊断缺陷等作为问题。
  • 查看功能门标签下报告的问题(例如 F-or_patterns),看看您是否可以帮助解决其中任何问题。
    • 特别是,如果您可以帮助编写测试,我们将不胜感激。

感谢您的阅读,并祝您模式匹配愉快!