模式解构—Pattern Destructure,其对应的就是模式构造,因此模式解构是将原本组合起来的结构分解为单独的、局部的、原始的部分。
我们之前在学习tuple时,写过类似以下例子:
fn main() {
let t = (1, 's', "白质".to_string());
let (t_one, t_two, t_tree) = t;
println!("t_one: {}, t_two: {}, t_tree: {}", t_one, t_two, t_tree);
}
运行结果:
t_one: 1, t_two: s, t_tree: 白质
以上例子就是典型的“模式解构”。
let t = (1, 's', "白质".to_string()); //将三个元素组合到一起形成一个tuple
let (t_one, t_two, t_tree) = t; //刚好与以上相反,把一个组合数据结构,拆解成三个不同的变量
下面再举一个稍复杂些的例子:
struct A(i32, String);
struct B {
data1 : A,
data2 : bool
}
fn main() {
let xb = B { data1 : A(32, "模式".to_string()), data2 : true };
let B{
data1 : A(val1, val2),
data2 : val3
} = xb;
println!("val1: {}, val2: {}, val3: {}", val1, val2, val3);
}
运行结果:
val1: 32, val2: 模式, val3: true
模式结构不仅可以发生在let语句中,还可以发生在match、if let、while let、函数调用、闭包调用等情境中。
下面将逐一介绍。
我们从最开始的猜数字就一直在接触match,这对我们来说再熟悉不过了。
通过枚举来举一个例子:
enum data_type {
Int(i32),
Float(f32),
Str(String),
Char(char)
}
fn print_data_type(data : data_type) {
match data {
data_type::Int(val) => {
println!("Int: {}", val);
}
data_type::Float(val) => {
println!("Float: {}", val);
}
data_type::Str(val) => {
println!("Str: {}", val);
}
data_type::Char(val) => {
println!("Char: {}", val);
}
}
}
fn main() {
let a = data_type::Int(1);
let b = data_type::Float(3.3);
let c = data_type::Str("牛逼".to_string());
let d = data_type::Char('s');
print_data_type(a);
print_data_type(b);
print_data_type(c);
print_data_type(d);
}
运行结果:
Int: 1
Float: 3.3
Str: 牛逼
Char: s
但是如果我们把上面的Char分支删除:
match data {
data_type::Int(val) => {
println!("Int: {}", val);
}
data_type::Float(val) => {
println!("Float: {}", val);
}
data_type::Str(val) => {
println!("Str: {}", val);
}
}
编译出错:
error[E0004]: non-exhaustive patterns: `Char(_)` not covered
--> src\main.rs:9:11
|
1 | / enum data_type {
2 | | Int(i32),
3 | | Float(f32),
4 | | Str(String),
5 | | Char(char)
| | ---- not covered
6 | | }
| |_- `data_type` defined here
...
9 | match data {
| ^^^^ pattern `Char(_)` not covered
non-exhaustive patterns:不详尽的模式、不完整的模式
Rust要求match需要对所以情况做完整的、无遗漏的匹配。exhaustive是Rust模式匹配的重要特定。
我们还可以通过下划线来表达其他情况:
fn print_data_type(data : data_type) {
match data {
data_type::Int(val) => {
println!("Int: {}", val);
}
data_type::Float(val) => {
println!("Float: {}", val);
}
_ => {
println!("others");
}
// data_type::Str(val) => {
// println!("Str: {}", val);
// }
// data_type::Char(val) => {
// println!("Char: {}", val);
// }
}
}
运行结果:
Int: 1
Float: 3.3
others
others
因此,如果多个项目之间存在依赖关系,在上游库中对一个enum添加成员,是一个破坏兼容性的行为。增加成员后,很有可能导致下游的,atch语句编译出错。
为了解决以上问题,Rust提供了一个叫non_exhaustive的功能:
#[non_exhaustive]
pub enum A {
//就不详细写了
}
这样下游项目中就必须使用下划线才能完整匹配。
下划线还能够用在模式匹配的其他地方,用来表示一个占位符,虽然匹配到了但是忽略它的值的情况:
fn main() {
let t = (1, 's', "Rust编程之路");
let (a, _, b) = t;
println!("a: {}", a);
println!("b: {}", b);
}
运行结果:
a: 1
b: Rust编程之路
函数参数就具备模式解构的能力:
fn func((a, _, b) : (i32, char, &str)) {
println!("a: {}", a);
println!("b: {}", b);
}
fn main() {
let t = (1, 's', "Rust编程之路");
func(t);
}
运行结果:
a: 1
b: Rust编程之路
我们之前在使用Option时,如果需要取出里面的值,需要做以下操作:
match data {
Some(x) => {
func(x);
}
_ => {}
}
这样做是必需的,但是比较麻烦。我们还可以采用以下方式:
if data.is_some() {
let x = data.unwrap();
func(x);
}
首先通过is_some判断是Some,然后通过unwrap取出数据。这样好像不是很麻烦,但是需要做两次判断是否有值。
我们可以使用if-let语法:
if let Some(x) = data {
func(x);
}
if-let与match相比的优势在于:match需要完整匹配,而if-let则可以选择匹配感兴趣的某个特定分支,这样比较简单。
同理,while-let是同样的道理,暂不举例了。
这个前面已经举过例子了。
当我们需要绑定的是被匹配对象的引用时,可以使用ref关键字。
struct A(i32, f32);
fn main() {
let a = A(3, 3.3);
let A(ref x, ref y) = a;
}