查看原文
其他

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

coolder117 码农真经 2023-12-25

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

模式匹配

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

以下为javascript中 switch case 实例:

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代码为:

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实现示例:

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的例子。这是一个瞎几吧写的例子,但难保你可能在你的职业生涯中的某个时候写过这样的代码:

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模式匹配来编写同功能的代码:

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,但你可能已经使用了这种模式。

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的代码:

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 变量关联起来。

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。

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值的方式:

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!"),
}
}

感谢您的阅读!

继续滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存