这是写给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表达式的语言形式就是:
match VALUE {
PATTERN1 => EXPRESSION1 ,
PATTERN2 => EXPRESSION2 ,
PATTERN3 => EXPRESSION3 ,
}
胖箭头 => 语法可能会让你犹疑片刻,因为它与 JavaScript 的箭头函数有相似之处,但这里它们没有关联。最后一个使用下划线_的pattern叫做catchall pattern,类似于switch case的默认情况。每个pattern => EXPRESSION组合被称为匹配对。
上面的例子并没有真正表达出模式匹配有多有用。它只是看起来像用不同的switch-case语法的花哨命名。接下来,让我们来谈谈解析和枚举,以了解为什么模式匹配是有用的。
解构
解构是将数组或结构的内部字段提取到独立变量中的过程。如果你在JavaScript中使用过解构,那么在Rust中也非常类似。
以下为javascript中解构赋值的例子:
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:
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!" );
}
}
我们可以利用模式匹配重构以上代码:
fn main () {
let file = read_file ( "path/to/file" );
match file {
Some ( contents ) => println ! ( "{}" , contents ),
None => println ! ( "Empty!" ),
}
}
感谢您的阅读!