:::info 本章内容改编自《Programming Rust, 2nd Edition》的第11章。 ::: Pattern Matching的语法在前文中已经多次出现。现在,我们通过两个示例回顾一下。

  1. #[derive(Copy, Clone, Debug, PartialEq, Eq)]
  2. pub enum TimeUnit {
  3. Second, Minute, Hour, Day, Month, Year,
  4. }
  5. impl TimeUnit {
  6. /// Return the plural noun for this time unit.
  7. pub fn plural(self) -> &'static str {
  8. // 以下 match self { ... } 是一个match expression
  9. match self {
  10. // 如果self是一个Seond变体,则这个match expression的值就是"seconds"
  11. TimeUnit::Second => "seconds",
  12. // 含义同上
  13. TimeUnit::Minute => "minutes",
  14. TimeUnit::Hour => "hours",
  15. TimeUnit::Day => "days",
  16. TimeUnit::Month => "months",
  17. TimeUnit::Year => "years",
  18. }
  19. // 在这里,仍然可以访问到self变量
  20. // 因为,在这个示例中,self的类型是Copy type,不涉及所有权的转移
  21. // 但是,这并不表示:如果self的类型是Non-Copy type,则模式匹配后无法再读取self
  22. // 具体内容稍后会介绍
  23. }
  24. /// Return the singular noun for this time unit.
  25. pub fn singular(self) -> &'static str {
  26. self.plural().trim_end_matches('s')
  27. }
  28. }
  29. #[derive(Copy, Clone, Debug, PartialEq)]
  30. pub enum RoughTime {
  31. InThePast(TimeUnit, u32),
  32. JustNow,
  33. InTheFuture(TimeUnit, u32)
  34. }
  35. impl RoughTime {
  36. pub fn to_english(self) -> String {
  37. // 以下 match self { ... } 是一个match expression
  38. match self {
  39. // 如果self是一个InThePast变体,且第一个分量为Hour、第二个分量为1,
  40. // 则match成功,进而返回 => 右侧表达式的值
  41. RoughTime::InThePast(TimeUnit::Hour, 1) =>
  42. format!("an hour ago"),
  43. // 如果self是一个InThePast变体,且第二个分量为1,
  44. // 则match成功,然后将self的第一个分量赋给局部变量unit
  45. // 如果第一个分量不是 Copy type,那么,第一个分量的所有权会转移给unit
  46. // 最后,返回 => 右侧表达式的值
  47. RoughTime::InThePast(unit, 1) =>
  48. format!("a {} ago", unit.singular()),
  49. // 如果self是一个InThePast变体,则match成功
  50. // 然后,将self的两个分量分别赋给局部变量unit和count
  51. // 最后,返回 => 右侧表达式的值
  52. RoughTime::InThePast(unit, count) =>
  53. format!("{} {} ago", count, unit.plural()),
  54. RoughTime::JustNow =>
  55. format!("just now"),
  56. RoughTime::InTheFuture(TimeUnit::Hour, 1) =>
  57. format!("an hour from now"),
  58. RoughTime::InTheFuture(unit, 1) =>
  59. format!("a {} from now", unit.singular()),
  60. RoughTime::InTheFuture(unit, count) =>
  61. format!("{} {} from now", count, unit.plural()),
  62. }
  63. }
  64. }

Pattern Matching 不仅仅适用于Enum类型,也适用于其它类型。

Literal/Variable/Wildcard Pattern

  1. match meadow.count_rabbits() {
  2. 0 => {} // => 符号的左侧是一个 literal pattern(字面量模式)
  3. 1 => println!("One rabbit"), // => 符号的左侧是一个 literal pattern(字面量模式)
  4. n => println!("{} rabbits", n); // => 符号的左侧是一个 variable pattern(变量模式)
  5. };
  6. let calendar = match settings.get_string("calendar") {
  7. "gregorian" => Calendar::Gregorian, // => 符号的左侧是一个 literal pattern(字面量模式)
  8. "chinese" => Calendar::Chinese,
  9. "ethiopian" => Calendar::Ethiopian,
  10. other => return parse_error("calendar", other) // => 符号的左侧是一个 variable pattern(变量模式)
  11. };
  1. let caption = match photo.tagged_pet() {
  2. Pet::Tyrannosaur => "Tyrannosaur",
  3. Pet::Samoyed => "Samoyed",
  4. _ => "I'm cute"
  5. // 在上面这一行代码中,符号 _ 表示一个 wildcard pattern
  6. // wildcard pattern 实际上就是一个匿名的variable pattern
  7. };

Tuple/Struct Pattern

  1. fn describe_point(x: i32, y: i32) -> &'static str {
  2. use std::cmp::Ordering::*;
  3. match (x.cmp(&0), y.cmp(&0)) {
  4. (Equal , Equal ) => "在原点",
  5. (_ , Equal ) => "在x轴上",
  6. (Equal , _ ) => "在x轴上",
  7. (Greater, Greater) => "在第一象限",
  8. (Less , Greater) => "在第二象限",
  9. _ => "在其它象限"
  10. }
  11. }
  1. match balloon.location {
  2. Point { x: 0, y: height } => println!("x = 0, y = {}", height),
  3. Point { x: x, y: y } => println!("x = {}, y = {}", x, y)
  4. // 在上面这行代码中,符号 => 的左侧,也可修改为 Point { x, y }
  5. }
  1. match get_account(id) {
  2. // ...
  3. Some(Account {
  4. name, lang, // 这两个分量是我们所关注的
  5. id: _, status: _, address: _, birthday: _, eye_color: _
  6. }) => lang.show_custom_greeting(name)
  7. // 第3~6行中,我们用了很多wildcard pattern来匹配一些我们不关心的分量
  8. // 看起来非常的繁琐
  9. // 其实,上面 => 符号左侧的模式可以等价地书写为:Some(Account { name, lang, ..})
  10. }
  11. //下面的 match expression 使用了省略符,更为简洁
  12. match get_account(id) {
  13. // ...
  14. Some(Account { name, lang, .. }) => lang.show_custom_greeting(name)
  15. }

Array/Slice Pattern

  1. fn hsl_to_rgb(hsl: [u8, 3]) -> [u8; 3] {
  2. match hsl {
  3. [_, _, 0 ] => [0 , 0 , 0 ],
  4. [_, _, 255] => [255, 255, 255]
  5. // ... 余下代码省略
  6. }
  7. }
  1. fn greet_people(names: &[&str]) {
  2. match names {
  3. [] => println!("Hello, nobody."),
  4. [a] => println!("Hello, {}.", a),
  5. [a, b] => println!("Hello, {} and {}.", a, b),
  6. [a, .., b] => println!("Hello, everyone from {} to {}.", a, b)
  7. }
  8. }

Reference Pattern

首先,请看下面的代码示例:

  1. match account {
  2. Account { name, lang, .. } => {
  3. ui.greet(&name, &lang);
  4. ui.show_settings(&account);
  5. }
  6. }

在上面这个示例中:

  • 如果Account的两个分量namelang都具有Copy type,则上面的代码不会发生编译错误
    • accountnamelang分量的值会分别复制/拷贝给局部变量namelang
    • account对值的所有权不会不会发生变化。因此,在第4行代码中,&account表达式会成功获得变量account的引用
  • 但是,如果Account的两个分量namelang中的任何一个具有Non-Copy type,则上面的代码会发生编译错误:error: borrow of moved value
    • 不失一般性,假设name具有一个Non-Copy type,而lang具有一个Copy type
    • 在第2行代码的执行中,account.name对值的所有权会被转移给局部变量nameaccount.lang的值会被复制给局部变量lang;然后,account中未发生所有权转移的值全部都会被释放(dropped)
    • 然后,在第4行代码的表达式&account中,我们又尝试获得一个account变量的引用;编译器看到这种不合法的操作后,报出编译错误

上面的代码示例反映出关于模式匹配的一种特殊需求:我们想匹配到某个值,并获得其中包含的若干值的引用;但是,我们并不想改变这个值的所有权(即:在模式匹配完成后,这个值的所有者并没有发生变化)。
Rust提供了 ref pattern来实现这种需求。请看下面的代码示例:

  1. match account {
  2. Account { ref name, ref lang, .. } => {
  3. ui.greet(name, lang);
  4. ui.show_settings(&account);
  5. }
  6. }

在上面的代码中:

  • 第2行代码中的 ref name 表示我们只想获得account.name的共享引用;在模式匹配成功后,name的类型是一种共享型引用。ref lang 含义类似
  • 同时,在模式匹配成功后,变量account对值的所有权没有发生任何变化。因此,在第4行代码中,我们可以正常获得account的共享引用

类似地,我们可以通过ref mut patten 在模式匹配中获得可变引用。请看如下的代码示例:

  1. match line_result {
  2. Err(ref err) => log_error(err), // err的类型为 &Error,一个共享引用类型
  3. Ok(ref mut line) => { // line的类型为 &mut String,一个可变引用类型
  4. trim_comments(line);
  5. handle(line);
  6. }
  7. }

与引用相关的另一种pattern是& pattern。请看下面的代码示例:

  1. // 假设方法调用返回一个引用 &Point3d { x: f32, y: f32, z: f32}
  2. match sphere.center() {
  3. &Point3d { x, y, z} => { }
  4. // 则上述模式会匹配成功,且 局部变量 x/y/z 会获得返回值中对应分量的拷贝
  5. }

需要注意一点:在上面的示例中,如果Point3d的任何一个分量具有非拷贝类型(Non-copy type),则上述代码示例会发生编译错误。原因在于,Rust不允许从一个共享引用中转移出值的所有权。在这种情况下,我们可以使用 & pattern 和 ref pattern的组合来避免这种编译错误。请看下面的代码示例:

  1. Some( &Car { ref engine, ... } ) => { }

在上面这个模式匹配中,我们尝试匹配一个Some(&Car)类型的值;如果匹配成功,则进一步获得一个&Car类型值中engine分量的一个引用,并将这个引用赋给局部变量engine

Match Guard

首先看一个代码示例:

  1. fn check_move(current_hex: Hex, click: Point) -> game::Result<Hex> {
  2. match point_to_hex(click) {
  3. None => Err("不合法的位置"),
  4. Some(current_hex) => Err("请点击一个新的位置"),
  5. Some(other_hex) => Ok(other_hex)
  6. }
  7. }

上面这个代码示例会产生编译错误。编译器应该会提示你最后一个模式分支永远不会被执行;因为它表达的模式与第二个模式分支中的模式是完全相同的。原因在于:第二个模式分支中的局部变量current_hex是一个全新的局部变量;同时,由于它与第一个参数名称同名,它还会覆盖(shadow)把第一个参数覆盖掉,导致无法在第二个模式分支中访问到第一个参数。
为了实现开发者的真实需求,我们需要把上面的代码示例修改为如下形式:

  1. fn check_move(current_hex: Hex, click: Point) -> game::Result<Hex> {
  2. match point_to_hex(click) {
  3. None => Err("不合法的位置"),
  4. Some(hex) => {
  5. if hex == current_hex {
  6. Err("请点击一个新的位置")
  7. } else {
  8. Ok(other_hex)
  9. }
  10. }
  11. }
  12. }

上面这种方式,在本质上是把两种模式的匹配合并到一个模式分支中,不太符合关注点分离的原则。我们可以使用match guard,仍然保留三个模式分支。请看如下代码示例:

  1. fn check_move(current_hex: Hex, click: Point) -> game::Result<Hex> {
  2. match point_to_hex(click) {
  3. None => Err("不合法的位置"),
  4. Some(hex) if hex == current_hex => Err("请点击一个新的位置"),
  5. Some(hex) => Ok(other_hex)
  6. }
  7. }

Matching Multiple Cases

有时候,我们需要用一个模式匹配分支匹配多种情况。Rust提供相应的语法来支持这种需求。请看下面的两个示例:

  1. let at_end = match chars.peek() {
  2. Some(&'\r') | Some(&'\n') | None => true,
  3. _ => false
  4. };
  1. match next_char {
  2. '0'..='9' => self.read_number(),
  3. 'a'..='z' | 'A'..='Z' => self.read_word(),
  4. ' ' | '\t' | '\n' => self.skip_whitespace(),
  5. _ => self.handle_punctutation()
  6. }

Binding with @ Pattern

话不多说。请看下面的代码示例:

  1. match self.get_selection() {
  2. Shape::Rect(top_left, bottom_right) => {
  3. optimized_paint(&Shape::Rect(top_left, bottom_right))
  4. }
  5. other_shape => {
  6. paint_outline(other_shape.get_outline())
  7. }
  8. }

上面match expression的第一个模式匹配分支具有明显的冗余操作:我们从一个tuple struct中取出了它的两个分量,然后又用这两个分量创建了一个相同的tuple struct。直接使用原来的tuple struct,不香吗?
使用@ pattern,上述第一个模式匹配分支可以书写为如下更简洁高效的形式:

  1. rect @ Shape::Rect(..) => { optimized_paint(&rect) }

下面给出了@ pattern的另一个示例:

  1. match chars.next() {
  2. Some(digit @ '0'..='9') => read_number(digit, chars),
  3. // 以下省略了一些代码
  4. }

Pattern Matching的其它使用场景

Pattern matching不仅可以出现在match expression中,也可以在其它很多地方使用。请看下面的代码示例:

  1. let Track { album, tracker_number, title, .. } = song;
  2. // 在上面的赋值语句中,通过模式匹配,把一个Track值中的多个分量赋值给多个对应的局部变量
  3. fn distance_to((x, y): (f64, f64)) -> f64 {
  4. // 这里省略了函数的实现代码
  5. }
  6. // 在上面的函数声明中,通过模式匹配,把一个2-tuple值的两个分量分别赋值给两个局部变量x、y
  7. // 然后,在函数体中可以直接访问这两个局部变量
  8. for (id, doc) in &cache_map {
  9. println!("Document #{}: {}", id, doc.title);
  10. }
  11. // 在上面的for in语句中,遍历到的每一个值中的分量直接被赋值给局部变量id和doc
  12. let sum = numbers.fold(0, |a, &num| a + num);
  13. // 在上面的语句中,一个引用类型的闭包参数,通过模式匹配,被去引用为相应的值
  14. if let RoughTime::InTheFuture(_, _) = user.data_of_birth() {
  15. user.set_time_traveler(true);
  16. }
  17. if let Some(document) = cache_map.get(&id) {
  18. return send_cached_response(document);
  19. }
  20. while let Err(err) = present_cheesy_anti_robot_task() {
  21. log_robot_attempt(err);
  22. }
  23. while let Some(_) = line_peek() {
  24. read_paragraph(&mut lines);
  25. }

一个示例

下面,我们通过更完整的一段代码展示模式匹配的使用:

  1. enum BinaryTree<T> {
  2. Empty,
  3. NonEmpty(Box<TreeNode<T>>),
  4. }
  5. struct TreeNode<T> {
  6. element: T,
  7. left: BinaryTree<T>,
  8. right: BinaryTree<T>,
  9. }
  10. impl<T: Ord> BinaryTree<T> {
  11. fn add(&mut self, value: T) {
  12. match *self {
  13. BinaryTree::Empty => {
  14. *self = BinaryTree::NonEmpty(Box::new(TreeNode {
  15. element: value,
  16. left: BinaryTree::Empty,
  17. right: BinaryTree::Empty,
  18. }))
  19. }
  20. BinaryTree::NonEmpty(ref mut node) => {
  21. if value <= node.element {
  22. node.left.add(value);
  23. } else {
  24. node.right.add(value);
  25. }
  26. }
  27. }
  28. }
  29. }
  30. let mut tree = BinaryTree::Empty;
  31. tree.add("Zhang San");
  32. tree.add("Li Si");
  33. tree.add("Wang Wu");

:::warning 本章内容到此结束 :::