< 返回版块

苦瓜小仔 发表于 2021-08-18 16:10

Tags:模式 Pattern

原文:Slice Patterns (by MICHAEL-F-BRYAN)

译者注:这是一篇结合切片和模式匹配的范例,它运用了 Rust Book: Patterns and Matching 篇章的知识。Rust 的类型系统和模式匹配相辅相成,呈现出简洁而强大、抽象而具体的表达力。

Rust 1.26 引入了一个漂亮的小功能,称为 Basic Slice Patterns (基础切片模式),它可以让你在 已知长度的切片上 进行模式匹配。后来在 Rust 1.42 中 ,这被扩展为允许使用 .. 语法匹配“其他所有东西”。

在许多其他功能发展的时候,增加 .. 语法匹配看起来只是一个小小的补充,但它为开发人员提供了编写更具表现力的代码的机会。

本文中编写的代码都附有 playground 链接。如果您发现这很有用或发现文章中的错误,请在博客的 issue 上告诉我!

处理复数 (plurality)

切片模式最简单的应用之一是通过匹配固定长度的切片来提供友好的消息。

通常,这在判断是否有 0、1 或更多条目(即复数)来自定义语句时很好用。例如:

fn print_words(sentence: &str) {
    let words: Vec<_> = sentence.split_whitespace().collect();

    match words.as_slice() {
        [] => println!("There were no words"),
        [word] => println!("Found 1 word: {}", word),
        _ => println!("Found {} words: {:?}", words.len(), words),
    }
}

fn main() {
    print_words("");
    print_words("Hello");
    print_words("Hello World!");
}

playground

... 将生成此输出:

There were no words
Found 1 word: Hello
Found 2 words: ["Hello", "World!"]

匹配切片的开始

.. 语法称为 rest 模式,可让您匹配切片的其余部分。

根据 ELF 格式,所有 ELF 二进制文件都必须以序列开头 0x7f ELF

我们可以利用这个事实和 rest 模式来实现 is_elf() 检查。

use std::error::Error;

fn is_elf(binary: &[u8]) -> bool {
    match binary {
        [0x7f, b'E', b'L', b'F', ..] => true,
        _ => false,
    }
}

fn main() -> Result<(), Box<dyn Error>> {
    let current_exe = std::env::current_exe()?;
    let binary = std::fs::read(&current_exe)?;

    if is_elf(&binary) {
        print!("{} is an ELF binary", current_exe.display());
    } else {
        print!("{} is NOT an ELF binary", current_exe.display());
    }

    Ok(())
}

playground

检查回文 (palindromes)

译者注:英语里回文和中文的古诗词回文不同,palindromes 指“正反读都一样的词语”。具体例子见 知乎

编程中一个非常常见的入门挑战是编写回文检查。

我们可以利用 @ 符号将匹配的任何内容绑定到一个新变量,运用在切片的开头和结尾进行匹配,来创建一个特别优雅的 is_palindrome() 函数。

fn is_palindrome(items: &[char]) -> bool {
    match items {
        [first, middle @ .., last] => first == last && is_palindrome(middle),
        [] | [_] => true,
    }
}

playground

一个简陋的 args 解析器

您可能想要使用切片模式的另一种方法是“剥离出”所需的前缀或后缀。

虽然已有 clap structopt 这样复杂命令行解析工具,但我们可以使用切片模式来实现自己的基本参数解析器。

fn parse_args(mut args: &[&str]) -> Args {
    let mut input = String::from("input.txt");
    let mut count = 0;

    loop {
        match args {
            ["-h" | "--help", ..] => {
                eprintln!("Usage: main [--input <filename>] [--count <count>] <args>...");
                std::process::exit(1);
            }
            ["-i" | "--input", filename, rest @ ..] => {
                input = filename.to_string();
                args = rest;
            }
            ["-c" | "--count", c, rest @ ..] => {
                count = c.parse().unwrap();
                args = rest;
            }
            [..] => break,
        }
    }

    let positional_args = args.iter().map(|s| s.to_string()).collect();

    Args {
        input,
        count,
        positional_args,
    }
}

struct Args {
    input: String,
    count: usize,
    positional_args: Vec<String>,
}

playground

irrefutable 模式匹配

虽然技术上讲这不是 切片模式 功能的一部分,但你可以使用模式匹配来解构 matchif let 语句之外的固定数组。

这对于避免基于永远不会失败的索引的笨重序列很有用。

fn format_coordinates([x, y]: [f32; 2]) -> String {
    format!("{}|{}", x, y)
}

fn main() {
    let point = [3.14, -42.0];

    println!("{}", format_coordinates(point));

    let [x, y] = point;
    println!("x: {}, y: {}", x, y);
    // Much more ergonomic than writing this!
    // let x = point[0];
    // let y = point[1];
}

playground


译者注:如果你对 (ir)refutable 不熟悉,那么你应该仔细看看 Rust Book: refutability 部分。以下是我对 refutablity 的总结。

refutable 和 irrefutable 是模式匹配中的两种形式:

  1. refutable :对某些值进行匹配可能会失败 。can fail to match for some possible value
  2. irrefutable :能匹配任何传递的可能值。match for any possible value passed

refutable

这个词直白地翻译叫做 “可反驳的”,而我倾向于使用“挂一漏万”来理解。

比如 if let Some(x) = a_value 表达式中的 Some(x);如果变量 a_value 中的值是 None 而不是 Some,那么 Some(x) 模式不能匹配。

if letwhile let 表达式被限制为只能接受可反驳的模式,因为根据定义他们意在处理可能的失败:条件表达式的功能就是根据成功或失败执行不同的操作。

match 匹配分支必须使用可反驳模式,除了最后一个分支需要使用能匹配任何剩余值的不可反驳模式。


在可反驳模式的地方使用不可反驳模式的例子:

if let x = 5 {
    println!("{}", x);
};

// 编译器警告:将不可反驳模式用于 `if let` 是没有意义的
warning: irrefutable if-let pattern
 --> <anon>:2:5
  |
2 | /     if let x = 5 {
3 | |     println!("{}", x);
4 | | };
  | |_^
  |
  = note: #[warn(irrefutable_let_patterns)] on by default

irrefutable

这个词直白地翻译叫做 “不可反驳的”,而我倾向于使用“万无一失”来理解。

比如 let x = 5; 语句中的 x,因为 x 可以匹配任何值所以不可能会失败。

函数参数、 let 语句和 for 循环 只能接受不可反驳的模式,因为通过不匹配的值程序无法进行有意义的工作。

这篇文章的例子就用到了 “函数参数”的情况。


在不可反驳模式的地方使用可反驳模式的例子:

let Some(x) = some_option_value;

// 编译器报错:
error[E0005]: refutable pattern in local binding: `None` not covered
 -->
  |
3 | let Some(x) = some_option_value;
  |     ^^^^^^^ pattern `None` not covered

如果 some_option_value 的值是 None,其不会成功匹配模式 Some(x),表明这个模式是可反驳的。

然而 let 语句只能接受不可反驳模式因为代码不能通过 None 值进行有效的操作。

因为我们没有覆盖(也不可能覆盖)到模式 Some(x) 的每一个可能的值, 所以 Rust 会合理地抗议。

修复:

if let Some(x) = some_option_value {
    println!("{}", x);
}

结论

就 Rust 切片模式中的功能而言,它并不过分复杂,但如果使用得当,它们确实可以提高代码的表现力。

这比我平时的深度潜水要短得多,但希望你学到了一些新东西。展望未来,我希望创建更多这些 Daily Rust 帖子,无耻地复制 Jonathan Boccara 的 Daily C++


Ext Link: https://adventures.michaelfbryan.com/posts/daily/slice-patterns

评论区

写评论
Shieber 2021-08-18 21:21

可以,很强。

1 共 1 条评论, 1 页