Skip to main content

模式解构

简介

模式解构(Pattern Destructure)是Rust中的一个重要设计,用于将复杂的数据结构拆解为单独的部分。与“Destructor”(析构器)不同,模式解构的作用是将一个数据结构拆解为独立的部分,而不是销毁对象。

示例

let tuple = (1_i32, false, 3f32);
let (head, center, tail) = tuple;

上面的代码展示了一个典型的“模式解构”。

  • 第一句话是“构造”,它把三个元素组合到了一起,形成了一个tuple。

  • 第二句代码则是把一个组合数据结构拆解开来,分成了三个不同的变量。

模式解构的原则是:**构造和解构遵循类似的语法。**我们可以使用相同的方式将数据结构组合和拆解。

更复杂的例子:

struct T1(i32, char);
struct T2 {
item1: T1,
item2: bool,
}

fn main() {
let x = T2 {
item1: T1(0, 'A'),
item2: false,
};
let T2 {
item1: T1(value1, value2),
item2: value3,
} = x;
println!("{} {} {}", value1, value2, value3);
}
  • 我们首先构造了一个T2类型的变量x,它内部又嵌套包含了其他的结构体。实际上,我们完全可以一次性解构多个层次,直接把这个对象内部深处的元素拆解出来。
  • 第二条let语句,就是一个比较复杂的“模式解构”,赋值号的左边不仅仅是一个变量名,还是一个完整的“模式”,在这个模式中引入了三个变量value1value2value3,分别绑定到了item1内部的两个成员以及item2上。
  • 编译,执行,打印出来的结果为"0 A false"。

match表达式

match是Rust中用于模式匹配的强大工具。它不仅可以匹配数据结构,还可以匹配具体的值。

首先,我们看看使用match的最简单的示例:

enum Direction {
East, West, South, North
}

fn print(x: Direction) {
match x {
Direction::East => println!("East"),
Direction::West => println!("West"),
Direction::South => println!("South"),
Direction::North => println!("North"),
}
}

fn main() {
let x = Direction::East;
print(x); //East
}

在这个例子中,根据Direction枚举的值,match表达式分别打印不同的方向。

完整性检查(Exhaustive)

exhaustive意思是无遗漏的、穷尽的、彻底的、全面的。Rust要求match表达式必须穷尽所有可能的情况。如果有遗漏,会导致编译错误。

如果我们把上例中的Direction::North对应的分支删除:

match x {
Direction::East => println!("East"),
Direction::West => println!("West"),
Direction::South => println!("South"),
}

编译时会出现编译错误:

error[E0004]: non-exhaustive patterns: `North` not covered

可以使用下划线 _ 作为默认分支来匹配未列出的情况:

match x {
Direction::East => println!("East"),
Direction::West => println!("West"),
Direction::South => println!("South"),
_ => println!("Other"),
}

non_exhaustive

在多个项目之间有依赖关系的时候,在上游的一个库中对enum增加成员,是一个破坏兼容性的改动。因为增加成员后,很可能会导致下游的使用者match语句编译不过。为解决这个问题,Rust提供了一个叫作non_exhaustive的功能(目前还没有稳定)。示例如下:

#[non_exhaustive]
pub enum Error {
NotFound,
PermissionDenied,
ConnectionRefused,
}

上游库作者可以用一个叫作non_exhaustive的attribute来标记一个enum或者struct,这样在另外一个项目中使用这个类型的时候,无论如何都没办法在match表达式中通过列举所有的成员实现完整匹配,必须使用下划线才能完成编译。这样,以后上游库里面为这个类型添加新成员的时候,就不会导致下游项目中的编译错误了,因为它已经存在一个默认分支匹配其他情况。

匹配范围和多个条件

match表达式可以匹配范围或多个条件:

fn category(x: i32) {
match x {
-1 | 1 => println!("true"),
0 => println!("false"),
_ => println!("error"),
}
}

fn main() {
let x = 1;
category(x);
}

使用范围匹配:

let x = 'X';
match x {
'a' ..= 'z' => println!("lowercase"),
'A' ..= 'Z' => println!("uppercase"),
_ => println!("something else"),
}

匹配守卫(Guards)

match表达式中可以使用 if 作为“匹配守卫”(guards)

示例

enum OptionalInt {
Value(i32),
Missing,
}

let x = OptionalInt::Value(5);
match x {
OptionalInt::Value(i) if i > 5 => println!("Got an int bigger than five!"),
OptionalInt::Value(..) => println!("Got an int!"),
OptionalInt::Missing => println!("No such luck."),
}

在这个例子中,OptionalInt 枚举有两个变体:ValueMissing。我们使用 match 表达式来匹配变量 x 的值,并在匹配 OptionalInt::Value(i) 时添加了一个 if i > 5 的条件。只有在 i 的值大于 5 时,才会匹配第一个分支并打印相应信息。

具体解释如下:

  1. 匹配守卫的使用
OptionalInt::Value(i) if i > 5 => println!("Got an int bigger than five!"),

这一行代码表示:如果 x 匹配到 OptionalInt::Value(i),并且 i 大于5,那么执行 println!("Got an int bigger than five!");

  1. 默认处理分支
OptionalInt::Value(..) => println!("Got an int!"),

如果 x 匹配到 OptionalInt::Value,但不满足守卫条件 if i > 5,则执行 println!("Got an int!");。这里的 .. 表示忽略 Value 中的值。

  1. 处理其他情况
OptionalInt::Missing => println!("No such luck."),

如果 x 匹配到 OptionalInt::Missing,则执行 println!("No such luck.");

守卫条件的覆盖范围

编译器在检查匹配守卫时,会确保所有情况都被覆盖。如果某些情况未被覆盖,编译器会报错。例如:

fn main() {
let x = 10;

match x {
i if i > 5 => println!("bigger than five"),
i if i <= 5 => println!("small or equal to five"),
}
}

尽管看似所有情况都已被覆盖,编译器仍会报错:

error[E0004]: non-exhaustive patterns: `x` not covered

解决方案是添加一个 _ 分支来处理所有未被显式匹配的情况:

match x {
i if i > 5 => println!("bigger than five"),
i if i <= 5 => println!("small or equal to five"),
_ => unreachable!(),
}

使用@绑定变量

@符号用于绑定变量,以便在匹配守卫中使用。下面是一个示例:

fn main() {
let x = 5;

match x {
e @ 1..=5 if e % 2 == 0 => println!("Even number in range: {}", e),
e @ 1..=5 => println!("Odd number in range: {}", e),
_ => println!("Out of range"),
}
}
  • e是变量名
  • e @ 1..=5 if e % 2 == 0 表示:如果 x 在1到5之间且是偶数,执行 println!("Even number in range: {}", e);
  • e @ 1..=5 表示:如果 x 在1到5之间但不是偶数,执行 println!("Odd number in range: {}", e);
  • _ 表示:处理所有其他未匹配的情况。

refmut

使用 ref 关键字绑定引用,避免所有权转移:

let x = 5_i32;
match x {
ref r => println!("Got a reference to {}", r),
}

使用 mut 关键字创建可变绑定:

fn main() {
let mut v = vec![1i32, 2, 3];
v = vec![4i32, 5, 6];
}

if-letwhile-let

if-letwhile-let 提供了简洁的模式匹配方式

if-let 语法

if-let 语法用于在条件判断时进行模式匹配。它的形式为:

fn main() {
let some_option = Some(5);

if let Some(x) = some_option {
println!("Matched, x = {}", x);
} else {
println!("Did not match");
}
}
  • if let Some(x) = some_option 表示如果 some_optionSome 类型,则匹配成功,并将 x 绑定到 Some 中的值。
  • 如果匹配成功,则打印 x 的值;否则执行 else 块中的代码。

while-let 语法

while-let 语法用于在循环条件判断时进行模式匹配。它的形式为:

fn main() {
let mut stack = Vec::new();
// 向堆栈中添加元素
stack.push(1);
stack.push(2);
stack.push(3);

// 使用while let 循环从堆栈中移除元素
while let Some(top) = stack.pop() {
println!("Popped: {}", top);
}
}

stack.pop() 返回一个 Option<T> 类型的值:

  • 如果堆栈中有元素,返回 Some(value),其中 value 是堆栈顶部的元素。
  • 如果堆栈为空,返回 None
  • while let Some(top) = stack.pop() 表示当 stack.pop() 返回 Some 值时,继续循环,并将 top 绑定到弹出的值。
  • 如果 stack.pop() 返回 None,循环结束。

函数和闭包参数模式解构

在Rust中,函数和闭包的参数也可以使用模式解构,这使得函数和闭包在接受复杂数据结构时更加灵活。

函数参数模式解构

你可以在函数参数中直接使用模式解构,来拆解传入的复杂数据结构。

struct Point {
x: i32,
y: i32,
}

fn print_coordinates(&(x, y): &(i32, i32)) {
println!("Current location: ({}, {})", x, y);
}

fn main() {
let point = (3, 5);
print_coordinates(&point);
}
  • 函数 print_coordinates 的参数 &(x, y) 使用模式解构,直接拆解传入的元组引用。
  • 在函数体中可以直接使用解构出的 xy

更复杂的例子,解构一个结构体:

struct Point {
x: i32,
y: i32,
}

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

let Point { x, y } = point;
println!("Point coordinates: ({}, {})", x, y);
}
  • let Point { x, y } = point; 解构 Point 结构体,将 xy 绑定到 point 的对应字段。

闭包参数模式解构

闭包参数也可以使用模式解构,这与函数参数模式解构类似。

闭包的基本结构

let add = |a, b| a + b;
  • let add =:这部分表示我们声明一个名为 add 的变量,并将一个闭包赋值给它。
  • |a, b|:这是闭包的参数列表,包含两个参数 ab。在Rust中,闭包的参数用竖线(|)包围。
  • a + b:这是闭包的函数体,表示对参数 ab 进行加法操作,并返回结果。

使用闭包

闭包 add 定义后,可以像普通函数一样调用它。例如:

fn main() {
let add = |a, b| a + b;
println!("{}", add(2, 3)); // 输出: 5
}

闭包捕获环境变量

闭包可以捕获其定义时的环境变量。

fn main() {
let x = 5;
let add_x = |a| a + x; // 捕获环境变量 x
println!("{}", add_x(3)); // 输出: 8
}

  • 闭包 add_x 定义在包含变量 x 的环境中。
  • 闭包 add_x 捕获了环境变量 x,可以在闭包体中使用它。
  • 当我们调用 add_x(3) 时,闭包计算 3 + 5,返回结果 8

闭包类型推导

Rust能够根据闭包的使用场景自动推导其参数和返回值的类型。例如:

let add = |a, b| a + b;

在这个闭包中,Rust会根据闭包的第一次调用自动推导 ab 的类型。如果第一次调用是 add(2, 3),Rust会推导出 ab 的类型为 i32

解构元组

let points = vec![(0, 0), (1, 1), (2, 2)];

let sum_of_squares: i32 = points.iter().map(|&(x, y)| x * x + y * y).sum();
println!("Sum of squares: {}", sum_of_squares);
  • points 是一个包含元组的向量。
  • points.iter() 返回一个迭代器,map 方法接受一个闭包作为参数。
  • 闭包参数 &(x, y) 使用模式解构,直接拆解传入的元组引用。
  • 在闭包体中,xy 是解构出的元组元素,计算它们的平方和。

解构结构体

struct Point {
x: i32,
y: i32,
}

let points = vec![
Point { x: 0, y: 0 },
Point { x: 1, y: 1 },
Point { x: 2, y: 2 },
];

let sum_of_coordinates: i32 = points.iter().map(|&Point { x, y }| x + y).sum();
println!("Sum of coordinates: {}", sum_of_coordinates);

  • points 是一个包含 Point 结构体的向量。
  • points.iter() 返回一个迭代器,map 方法接受一个闭包作为参数。
  • 闭包参数 &Point { x, y } 使用模式解构,直接拆解传入的结构体引用。
  • 在闭包体中,xy 是解构出的结构体字段,计算它们的和。

总结

  • 模式解构:Rust中的模式解构功能允许将复杂的数据结构拆解为独立的部分。这种解构方式在 letmatchif-letwhile-let 语句以及函数和闭包参数中均适用。
  • if-letwhile-let:这两个语法糖简化了在条件判断和循环条件中的模式匹配,避免了使用完整的 match 语句。if-let 用于条件判断,while-let 用于循环条件判断。
  • 函数和闭包参数模式解构:函数和闭包参数可以使用模式解构,允许直接在参数位置解构复杂的数据结构,使函数和闭包体内的处理更加简洁。
  • 环境捕获:闭包可以捕获其定义时的环境变量,使其在闭包体内使用这些变量,增加了闭包的灵活性。
  • 类型推导:Rust能够根据闭包的使用场景自动推导其参数和返回值的类型,无需显式声明类型。
  • 灵活性和可读性:模式解构和闭包参数模式解构大大提高了代码的灵活性和可读性,使得Rust在处理复杂数据结构时更加简洁和高效。