:::info 本章内容改编自《Programming Rust, 2nd Edition》的第11章。 ::: Pattern Matching的语法在前文中已经多次出现。现在,我们通过两个示例回顾一下。
#[derive(Copy, Clone, Debug, PartialEq, Eq)]pub enum TimeUnit {Second, Minute, Hour, Day, Month, Year,}impl TimeUnit {/// Return the plural noun for this time unit.pub fn plural(self) -> &'static str {// 以下 match self { ... } 是一个match expressionmatch self {// 如果self是一个Seond变体,则这个match expression的值就是"seconds"TimeUnit::Second => "seconds",// 含义同上TimeUnit::Minute => "minutes",TimeUnit::Hour => "hours",TimeUnit::Day => "days",TimeUnit::Month => "months",TimeUnit::Year => "years",}// 在这里,仍然可以访问到self变量// 因为,在这个示例中,self的类型是Copy type,不涉及所有权的转移// 但是,这并不表示:如果self的类型是Non-Copy type,则模式匹配后无法再读取self// 具体内容稍后会介绍}/// Return the singular noun for this time unit.pub fn singular(self) -> &'static str {self.plural().trim_end_matches('s')}}#[derive(Copy, Clone, Debug, PartialEq)]pub enum RoughTime {InThePast(TimeUnit, u32),JustNow,InTheFuture(TimeUnit, u32)}impl RoughTime {pub fn to_english(self) -> String {// 以下 match self { ... } 是一个match expressionmatch self {// 如果self是一个InThePast变体,且第一个分量为Hour、第二个分量为1,// 则match成功,进而返回 => 右侧表达式的值RoughTime::InThePast(TimeUnit::Hour, 1) =>format!("an hour ago"),// 如果self是一个InThePast变体,且第二个分量为1,// 则match成功,然后将self的第一个分量赋给局部变量unit// 如果第一个分量不是 Copy type,那么,第一个分量的所有权会转移给unit// 最后,返回 => 右侧表达式的值RoughTime::InThePast(unit, 1) =>format!("a {} ago", unit.singular()),// 如果self是一个InThePast变体,则match成功// 然后,将self的两个分量分别赋给局部变量unit和count// 最后,返回 => 右侧表达式的值RoughTime::InThePast(unit, count) =>format!("{} {} ago", count, unit.plural()),RoughTime::JustNow =>format!("just now"),RoughTime::InTheFuture(TimeUnit::Hour, 1) =>format!("an hour from now"),RoughTime::InTheFuture(unit, 1) =>format!("a {} from now", unit.singular()),RoughTime::InTheFuture(unit, count) =>format!("{} {} from now", count, unit.plural()),}}}
Pattern Matching 不仅仅适用于Enum类型,也适用于其它类型。
Literal/Variable/Wildcard Pattern
match meadow.count_rabbits() {0 => {} // => 符号的左侧是一个 literal pattern(字面量模式)1 => println!("One rabbit"), // => 符号的左侧是一个 literal pattern(字面量模式)n => println!("{} rabbits", n); // => 符号的左侧是一个 variable pattern(变量模式)};let calendar = match settings.get_string("calendar") {"gregorian" => Calendar::Gregorian, // => 符号的左侧是一个 literal pattern(字面量模式)"chinese" => Calendar::Chinese,"ethiopian" => Calendar::Ethiopian,other => return parse_error("calendar", other) // => 符号的左侧是一个 variable pattern(变量模式)};
let caption = match photo.tagged_pet() {Pet::Tyrannosaur => "Tyrannosaur",Pet::Samoyed => "Samoyed",_ => "I'm cute"// 在上面这一行代码中,符号 _ 表示一个 wildcard pattern// wildcard pattern 实际上就是一个匿名的variable pattern};
Tuple/Struct Pattern
fn describe_point(x: i32, y: i32) -> &'static str {use std::cmp::Ordering::*;match (x.cmp(&0), y.cmp(&0)) {(Equal , Equal ) => "在原点",(_ , Equal ) => "在x轴上",(Equal , _ ) => "在x轴上",(Greater, Greater) => "在第一象限",(Less , Greater) => "在第二象限",_ => "在其它象限"}}
match balloon.location {Point { x: 0, y: height } => println!("x = 0, y = {}", height),Point { x: x, y: y } => println!("x = {}, y = {}", x, y)// 在上面这行代码中,符号 => 的左侧,也可修改为 Point { x, y }}
match get_account(id) {// ...Some(Account {name, lang, // 这两个分量是我们所关注的id: _, status: _, address: _, birthday: _, eye_color: _}) => lang.show_custom_greeting(name)// 第3~6行中,我们用了很多wildcard pattern来匹配一些我们不关心的分量// 看起来非常的繁琐// 其实,上面 => 符号左侧的模式可以等价地书写为:Some(Account { name, lang, ..})}//下面的 match expression 使用了省略符,更为简洁match get_account(id) {// ...Some(Account { name, lang, .. }) => lang.show_custom_greeting(name)}
Array/Slice Pattern
fn hsl_to_rgb(hsl: [u8, 3]) -> [u8; 3] {match hsl {[_, _, 0 ] => [0 , 0 , 0 ],[_, _, 255] => [255, 255, 255]// ... 余下代码省略}}
fn greet_people(names: &[&str]) {match names {[] => println!("Hello, nobody."),[a] => println!("Hello, {}.", a),[a, b] => println!("Hello, {} and {}.", a, b),[a, .., b] => println!("Hello, everyone from {} to {}.", a, b)}}
Reference Pattern
首先,请看下面的代码示例:
match account {Account { name, lang, .. } => {ui.greet(&name, &lang);ui.show_settings(&account);}}
在上面这个示例中:
- 如果
Account的两个分量name和lang都具有Copy type,则上面的代码不会发生编译错误account的name和lang分量的值会分别复制/拷贝给局部变量name和langaccount对值的所有权不会不会发生变化。因此,在第4行代码中,&account表达式会成功获得变量account的引用
- 但是,如果
Account的两个分量name和lang中的任何一个具有Non-Copy type,则上面的代码会发生编译错误:error: borrow of moved value- 不失一般性,假设
name具有一个Non-Copy type,而lang具有一个Copy type - 在第2行代码的执行中,
account.name对值的所有权会被转移给局部变量name;account.lang的值会被复制给局部变量lang;然后,account中未发生所有权转移的值全部都会被释放(dropped) - 然后,在第4行代码的表达式
&account中,我们又尝试获得一个account变量的引用;编译器看到这种不合法的操作后,报出编译错误
- 不失一般性,假设
上面的代码示例反映出关于模式匹配的一种特殊需求:我们想匹配到某个值,并获得其中包含的若干值的引用;但是,我们并不想改变这个值的所有权(即:在模式匹配完成后,这个值的所有者并没有发生变化)。
Rust提供了 ref pattern来实现这种需求。请看下面的代码示例:
match account {Account { ref name, ref lang, .. } => {ui.greet(name, lang);ui.show_settings(&account);}}
在上面的代码中:
- 第2行代码中的
ref name表示我们只想获得account.name的共享引用;在模式匹配成功后,name的类型是一种共享型引用。ref lang含义类似 - 同时,在模式匹配成功后,变量
account对值的所有权没有发生任何变化。因此,在第4行代码中,我们可以正常获得account的共享引用
类似地,我们可以通过ref mut patten 在模式匹配中获得可变引用。请看如下的代码示例:
match line_result {Err(ref err) => log_error(err), // err的类型为 &Error,一个共享引用类型Ok(ref mut line) => { // line的类型为 &mut String,一个可变引用类型trim_comments(line);handle(line);}}
与引用相关的另一种pattern是& pattern。请看下面的代码示例:
// 假设方法调用返回一个引用 &Point3d { x: f32, y: f32, z: f32}match sphere.center() {&Point3d { x, y, z} => { }// 则上述模式会匹配成功,且 局部变量 x/y/z 会获得返回值中对应分量的拷贝}
需要注意一点:在上面的示例中,如果Point3d的任何一个分量具有非拷贝类型(Non-copy type),则上述代码示例会发生编译错误。原因在于,Rust不允许从一个共享引用中转移出值的所有权。在这种情况下,我们可以使用 & pattern 和 ref pattern的组合来避免这种编译错误。请看下面的代码示例:
Some( &Car { ref engine, ... } ) => { }
在上面这个模式匹配中,我们尝试匹配一个Some(&Car)类型的值;如果匹配成功,则进一步获得一个&Car类型值中engine分量的一个引用,并将这个引用赋给局部变量engine。
Match Guard
首先看一个代码示例:
fn check_move(current_hex: Hex, click: Point) -> game::Result<Hex> {match point_to_hex(click) {None => Err("不合法的位置"),Some(current_hex) => Err("请点击一个新的位置"),Some(other_hex) => Ok(other_hex)}}
上面这个代码示例会产生编译错误。编译器应该会提示你最后一个模式分支永远不会被执行;因为它表达的模式与第二个模式分支中的模式是完全相同的。原因在于:第二个模式分支中的局部变量current_hex是一个全新的局部变量;同时,由于它与第一个参数名称同名,它还会覆盖(shadow)把第一个参数覆盖掉,导致无法在第二个模式分支中访问到第一个参数。
为了实现开发者的真实需求,我们需要把上面的代码示例修改为如下形式:
fn check_move(current_hex: Hex, click: Point) -> game::Result<Hex> {match point_to_hex(click) {None => Err("不合法的位置"),Some(hex) => {if hex == current_hex {Err("请点击一个新的位置")} else {Ok(other_hex)}}}}
上面这种方式,在本质上是把两种模式的匹配合并到一个模式分支中,不太符合关注点分离的原则。我们可以使用match guard,仍然保留三个模式分支。请看如下代码示例:
fn check_move(current_hex: Hex, click: Point) -> game::Result<Hex> {match point_to_hex(click) {None => Err("不合法的位置"),Some(hex) if hex == current_hex => Err("请点击一个新的位置"),Some(hex) => Ok(other_hex)}}
Matching Multiple Cases
有时候,我们需要用一个模式匹配分支匹配多种情况。Rust提供相应的语法来支持这种需求。请看下面的两个示例:
let at_end = match chars.peek() {Some(&'\r') | Some(&'\n') | None => true,_ => false};
match next_char {'0'..='9' => self.read_number(),'a'..='z' | 'A'..='Z' => self.read_word(),' ' | '\t' | '\n' => self.skip_whitespace(),_ => self.handle_punctutation()}
Binding with @ Pattern
话不多说。请看下面的代码示例:
match self.get_selection() {Shape::Rect(top_left, bottom_right) => {optimized_paint(&Shape::Rect(top_left, bottom_right))}other_shape => {paint_outline(other_shape.get_outline())}}
上面match expression的第一个模式匹配分支具有明显的冗余操作:我们从一个tuple struct中取出了它的两个分量,然后又用这两个分量创建了一个相同的tuple struct。直接使用原来的tuple struct,不香吗?
使用@ pattern,上述第一个模式匹配分支可以书写为如下更简洁高效的形式:
rect @ Shape::Rect(..) => { optimized_paint(&rect) }
下面给出了@ pattern的另一个示例:
match chars.next() {Some(digit @ '0'..='9') => read_number(digit, chars),// 以下省略了一些代码}
Pattern Matching的其它使用场景
Pattern matching不仅可以出现在match expression中,也可以在其它很多地方使用。请看下面的代码示例:
let Track { album, tracker_number, title, .. } = song;// 在上面的赋值语句中,通过模式匹配,把一个Track值中的多个分量赋值给多个对应的局部变量fn distance_to((x, y): (f64, f64)) -> f64 {// 这里省略了函数的实现代码}// 在上面的函数声明中,通过模式匹配,把一个2-tuple值的两个分量分别赋值给两个局部变量x、y// 然后,在函数体中可以直接访问这两个局部变量for (id, doc) in &cache_map {println!("Document #{}: {}", id, doc.title);}// 在上面的for in语句中,遍历到的每一个值中的分量直接被赋值给局部变量id和doclet sum = numbers.fold(0, |a, &num| a + num);// 在上面的语句中,一个引用类型的闭包参数,通过模式匹配,被去引用为相应的值if let RoughTime::InTheFuture(_, _) = user.data_of_birth() {user.set_time_traveler(true);}if let Some(document) = cache_map.get(&id) {return send_cached_response(document);}while let Err(err) = present_cheesy_anti_robot_task() {log_robot_attempt(err);}while let Some(_) = line_peek() {read_paragraph(&mut lines);}
一个示例
下面,我们通过更完整的一段代码展示模式匹配的使用:
enum BinaryTree<T> {Empty,NonEmpty(Box<TreeNode<T>>),}struct TreeNode<T> {element: T,left: BinaryTree<T>,right: BinaryTree<T>,}impl<T: Ord> BinaryTree<T> {fn add(&mut self, value: T) {match *self {BinaryTree::Empty => {*self = BinaryTree::NonEmpty(Box::new(TreeNode {element: value,left: BinaryTree::Empty,right: BinaryTree::Empty,}))}BinaryTree::NonEmpty(ref mut node) => {if value <= node.element {node.left.add(value);} else {node.right.add(value);}}}}}let mut tree = BinaryTree::Empty;tree.add("Zhang San");tree.add("Li Si");tree.add("Wang Wu");
:::warning 本章内容到此结束 :::
