Please enable Javascript to view the contents

写给javascript程序员的rust教程(四)模式匹配和枚举【译】

 ·  ☕ 6 分钟

这是写给javascript程序员的rust教程系列文章的第四部分,模式匹配和枚举。前三部分请戳:

模式匹配

要了解模式匹配,让我们从JavaScript中熟悉的内容-Switch Case开始。

以下为javascript中 switch case 实例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function print_color(color) {
  switch (color) {
    case "rose":
      console.log("roses are red,");
      break;
    case "violet":
      console.log("violets are blue,");
      break;
    default:
      console.log("sugar is sweet, and so are you.");
  }
}

print_color("rose"); // roses are red,
print_color("violet"); // violets are blue,
print_color("you"); // sugar is sweet, and so are you.

与之等效的rust代码为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fn print_color(color: &str) {
  match color {
    "rose" => println!("roses are red,"),
    "violet" => println!("violets are blue,"),
    _ => println!("sugar is sweet, and so are you."),
  }
}

fn main() {
  print_color("rose"); // roses are red,
  print_color("violet"); // violets are blue,
  print_color("you"); // sugar is sweet, and so are you.
}

看到以上代码,相信你已经秒懂了。是的没错,match表达式的语言形式就是:

1
2
3
4
5
match VALUE {
  PATTERN1 => EXPRESSION1,
  PATTERN2 => EXPRESSION2,
  PATTERN3 => EXPRESSION3,
}

胖箭头 => 语法可能会让你犹疑片刻,因为它与 JavaScript 的箭头函数有相似之处,但这里它们没有关联。最后一个使用下划线_的pattern叫做catchall pattern,类似于switch case的默认情况。每个pattern => EXPRESSION组合被称为匹配对。

上面的例子并没有真正表达出模式匹配有多有用。它只是看起来像用不同的switch-case语法的花哨命名。接下来,让我们来谈谈解析和枚举,以了解为什么模式匹配是有用的。


解构

解构是将数组或结构的内部字段提取到独立变量中的过程。如果你在JavaScript中使用过解构,那么在Rust中也非常类似。

以下为javascript中解构赋值的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let rgb = [96, 172, 57];
let [red, green, blue] = rgb;
console.log(red); // 96
console.log(green); // 172
console.log(blue); // 57

let person = { name: "shesh", city: "singapore" };
let { name, city } = person;
console.log(name); // name
console.log(city); // city

rust实现示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
struct Person {
  name: String,
  city: String,
}

fn main() {
  let rgb = [96, 172, 57];
  let [red, green, blue] = rgb;
  println!("{}", red); // 96
  println!("{}", green); // 172
  println!("{}", blue); // 57

  let person = Person {
    name: "shesh".to_string(),
    city: "singapore".to_string(),
  };
  let Person { name, city } = person;
  println!("{}", name); // name
  println!("{}", city); // city
}

怎么样?是不是一毛一样!


比较结构 (Comparing Structs)

编写 “if this then that “类型的代码是很常见的。结合解构和模式匹配,我们可以用一种非常简洁的方式来编写这些类型的逻辑。

让我们来看看下面这个JavaScript的例子。这是一个瞎几吧写的例子,但难保你可能在你的职业生涯中的某个时候写过这样的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const point = { x: 0, y: 30 };
const { x, y } = point;

if (x === 0 && y === 0) {
  console.log("both are zero");
} else if (x === 0) {
  console.log(`x is zero and y is ${y}`);
} else if (y === 0) {
  console.log(`x is ${x} and y is zero`);
} else {
  console.log(`x is ${x} and y is ${y}`);
}

让我们用Rust模式匹配来编写同功能的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct Point {
  x: i32,
  y: i32,
}

fn main() {
  let point = Point { x: 10, y: 0 };

  match point {
    Point { x: 0, y: 0 } => println!("both are zero"),
    Point { x: 0, y } => println!("x is zero and y is {}", y),
    Point { x, y: 0 } => println!("x is {} and y is zero", x),
    Point { x, y } => println!("x is {} and y is {}", x, y),
  }
}

与if else逻辑相比,模式匹配相对简洁,但也可能会让人感到困惑,因为以上代码要同时执行值比较、解构和赋值。

以下为代码可视化解释:

我们开始明白为什么叫 “模式(图案)匹配 “了–我们拿一个输入,看看匹配对中哪个图案更 “适合”–这就像孩子们玩的形状分拣器玩具一样。除了比较,我们还在第二、三、四条匹配对中进行变量绑定。我们将变量x或y或两者都传递给各自的表达式。

模式匹配也是详尽的–也就是说,它迫使你处理所有可能的情况。试着去掉最后一个匹配对,Rust就不会让你编译代码。


枚举

JavaScript没有枚举(Enums)类型,但如果你使用过TypeScript,你可以把Rust的Enums看作是TypeScript的Enums和TypeScript的Discriminated Unions的结合。

在最简单的情况下,Enums可以作为一组常量使用。

例如,尽管JavaScript没有Enums,但你可能已经使用了这种模式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const DIRECTION = {
  FORWARD: "FORWARD",
  BACKWARD: "BACKWARD",
  LEFT: "LEFT",
  RIGHT: "RIGHT",
};

function move_drone(direction) {
  switch (direction) {
    case DIRECTION.FORWARD:
      console.log("Move Forward");
      break;
    case DIRECTION.BACKWARD:
      console.log("Move Backward");
      break;
    case DIRECTION.LEFT:
      console.log("Move Left");
      break;
    case DIRECTION.RIGHT:
      console.log("Move Right");
      break;
  }
}

move_drone(DIRECTION.FORWARD); // "Move Forward"

在这里,我们本可以将FORWARD、BACKWARD、LEFT和RIGHT定义为单独的常量,将其归入DIRECTION对象中,有以下好处:

FORWARD, BACKWARD, LEFT和RIGHT这几个名字在DIRECTION对象下是有命名间隔的,所以可以避免命名冲突。

它是自我表述的,因为我们可以顾名思义的知道所有可用的有效方向

但是,这种方法存在一些问题:

  • 如果有人将NORTH或UP作为参数传递给move_drone函数怎么办?为了解决这个问题,我们可以添加一个验证,以确保只有存在于DIRECTION对象中的值才被允许在移动函数中使用。
  • 如果将来我们决定支持 UP 和 DOWN,或者将 LEFT/RIGHT 改名为 PORT/STARBOARD 呢?我们需要找到所有使用类似 switch-case 或 if-else 的地方。有可能我们会漏掉一些地方,这将导致生产中的问题。

在Rust等强类型语言中,Enums的功能十分强大,因为它们无需我们编写额外的代码就能解决上述问题。

如果一个函数只能接受一小部分有效的输入,那么Enums可以用来强制执行这个约束条件

带有模式匹配的Enums强制你覆盖所有情况。当你在未来更新Enums,这一点非常有用。

以下为Rust的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
enum Direction {
  Forward,
  Backward,
  Left,
  Right,
}

fn move_drone(direction: Direction) {
  match direction {
    Direction::Forward => println!("Move Forward"),
    Direction::Backward => println!("Move Backward"),
    Direction::Left => println!("Move Left"),
    Direction::Right => println!("Move Right"),
  }
}

fn main() {
  move_drone(Direction::Forward);
}

我们使用::符号来访问Enum内部的变量。试着通过调用 “move_drone(Direction::Up) “或在Direction enum中添加 “Down “作为新的项目来编辑这段代码。在第一种情况下,编译器会抛出一个错误,说 “Up “在 “Direction “中没有找到,而在第二种情况下,编译器会直接报错:我们在匹配块中没有覆盖 “Down”。

Rust Enums 能做的远不止是作为一组常量–我们还可以将数据与 Enum 变量关联起来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
enum Direction {
  Forward,
  Backward,
  Left,
  Right,
}

enum Operation {
  PowerOn,
  PowerOff,
  Move(Direction),
  Rotate,
  TakePhoto { is_landscape: bool, zoom_level: i32 },
}

fn operate_drone(operation: Operation) {
  match operation {
    Operation::PowerOn => println!("Power On"),
    Operation::PowerOff => println!("Power Off"),
    Operation::Move(direction) => move_drone(direction),
    Operation::Rotate => println!("Rotate"),
    Operation::TakePhoto {
      is_landscape,
      zoom_level,
    } => println!("TakePhoto {}, {}", is_landscape, zoom_level),
  }
}

fn move_drone(direction: Direction) {
  match direction {
    Direction::Forward => println!("Move Forward"),
    Direction::Backward => println!("Move Backward"),
    Direction::Left => println!("Move Left"),
    Direction::Right => println!("Move Right"),
  }
}

fn main() {
  operate_drone(Operation::Move(Direction::Forward));
  operate_drone(Operation::TakePhoto {
    is_landscape: true,
    zoom_level: 10,
  })
}

在这里,我们又添加了一个名为Operation的Enum,它包含了 “类似单元 “的变体(PowerOn、PowerOff、Rotate)和 “类似结构 “的变体(Move、TakePhoto)。请注意我们是如何使用模式匹配与解构和变量绑定的。

如果你使用过TypeScript或Flow,这类似于discriminated unions或sum。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
interface PowerOn {
  kind: "PowerOn";
}

interface PowerOff {
  kind: "PowerOff";
}

type Direction = "Forward" | "Backward" | "Left" | "Right";

interface Move {
  kind: "Move";
  direction: Direction;
}

interface Rotate {
  kind: "Rotate";
}

interface TakePhoto {
  kind: "TakePhoto";
  is_landscape: boolean;
  zoom_level: number;
}

type Operation = PowerOn | PowerOff | Move | Rotate | TakePhoto;

function operate_drone(operation: Operation) {
  switch (operation.kind) {
    case "PowerOn":
      console.log("Power On");
      break;
    case "PowerOff":
      console.log("Power Off");
      break;
    case "Move":
      move_drone(operation.direction);
      break;
    case "Rotate":
      console.log("Rotate");
      break;
    case "TakePhoto":
      console.log(`TakePhoto ${operation.is_landscape}, ${operation.zoom_level}`);
      break;
  }
}

function move_drone(direction: Direction) {
  switch (direction) {
    case "Forward":
      console.log("Move Forward");
      break;
    case "Backward":
      console.log("Move Backward");
      break;
    case "Left":
      console.log("Move Left");
      break;
    case "Right":
      console.log("Move Right");
      break;
  }
}

operate_drone({
  kind: "Move",
  direction: "Forward",
});

operate_drone({
  kind: "TakePhoto",
  is_landscape: true,
  zoom_level: 10,
});

Option

我们在第2部分,初步学习了Option类型。Option实际上是一个Enum类型,只有两个变量-Some和None:

1
2
3
4
enum Option<T> {
  Some(T),
  None,
}

回顾一下第2部分处理option值的方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
fn read_file(path: &str) -> Option<&str> {
  let contents = "hello";

  if path != "" {
    return Some(contents);
  }

  return None;
}

fn main() {
  let file = read_file("path/to/file");

  if file.is_some() {
    let contents = file.unwrap();
    println!("{}", contents);
  } else {
    println!("Empty!");
  }
}

我们可以利用模式匹配重构以上代码:

1
2
3
4
5
6
7
8
fn main() {
  let file = read_file("path/to/file");

  match file {
    Some(contents) => println!("{}", contents),
    None => println!("Empty!"),
  }
}

感谢您的阅读!

分享

码中人
作者
码中人
Web Developer