这是写给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!"),
}
}
|
感谢您的阅读!