模式解构
简介
模式解构(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
语句,就是一个比较复杂的“模式解构”,赋值号的左边不仅仅是一个变量名,还是一个完整的“模式”,在这个模式中引入了三个变量value1
、value2
、value3
,分别绑定到了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
枚举有两个变体:Value
和 Missing
。我们使用 match
表达式来匹配变量 x
的值,并在匹配 OptionalInt::Value(i)
时添加了一个 if i > 5
的条件。只有在 i
的值大于 5 时,才会匹配第一个分支并打印相应信息。
具体解释如下:
- 匹配守卫的使用
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!");
。
- 默认处理分支
OptionalInt::Value(..) => println!("Got an int!"),
如果 x
匹配到 OptionalInt::Value
,但不满足守卫条件 if i > 5
,则执行 println!("Got an int!");
。这里的 ..
表示忽略 Value
中的值。
- 处理其他情况
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);
。_
表示:处理所有其他未匹配的情况。
ref
和mut
使用 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-let
和while-let
if-let
和 while-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_option
是Some
类型,则匹配成功,并将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)
使用模式解构,直接拆解传入的元组引用。 - 在函数体中可以直接使用解构出的
x
和y
。
更复杂的例子,解构一个结构体:
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
结构体,将x
和y
绑定到point
的对应字段。
闭包参数模式解构
闭包参数也可以使用模式解构,这与函数参数模式解构类似。
闭包的基本结构
let add = |a, b| a + b;
let add =
:这部分表示我们声明一个名为add
的变量,并将一个闭包赋值给它。|a, b|
:这是闭包的参数列表,包含两个参数a
和b
。在Rust中,闭包的参数用竖线(|
)包围。a + b
:这是闭包的函数体,表示对参数a
和b
进行加法操作,并返回结果。
使用闭包
闭包 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会根据闭包的第一次调用自动推导 a
和 b
的类型。如果第一次调用是 add(2, 3)
,Rust会推导出 a
和 b
的类型为 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)
使用模式解构,直接拆解传入的元组引用。 - 在闭包体中,
x
和y
是解构出的元组元素,计算它们的平方和。