Rust Programming Cookbook
上QQ阅读APP看书,第一时间看更新

How to do it...

Just a few steps need to be followed in order to learn more about pattern matching:

  1. Create a new binary project using cargo new pattern-matching. This time, we'll run an actual executable! Again, open the project using Visual Studio Code or another editor. 
  2. Let's check out literal matching. Just like a switch-case statement in other languages, each matching arm can match to literals as well:
fn literal_match(choice: usize) -> String {
match choice {
0 | 1 => "zero or one".to_owned(),
2 ... 9 => "two to nine".to_owned(),
10 => "ten".to_owned(),
_ => "anything else".to_owned()
}
}
  1. However, pattern matching is way more powerful than that. For example, tuple elements can be extracted and selectively matched:
fn tuple_match(choices: (i32, i32, i32, i32)) -> String {
match choices {
(_, second, _, fourth) => format!("Numbers at positions 1
and 3 are {} and {} respectively", second, fourth)
}
}
  1. Destructuring (moving properties out of a struct into their own variables) is a powerful feature in conjunction with structs and enums. First, this facilitates the assigning of multiple variables in a single matching arm to values that are assigned to properties at the incoming struct instance. Now, let's define a few structs and enums:
enum Background {
Color(u8, u8, u8),
Image(&'static str),
}

enum UserType {
Casual,
Power
}

struct MyApp {
theme: Background,
user_type: UserType,
secret_user_id: usize
}

Then, the individual properties can be matched in a destructuring match. Enums work just as well—however, be sure to cover all possible variations; the compiler will notice (or use the special _ to match all). Matching is also done from top to bottom, so whichever rule applies first will be executed. The following snippet matches variations of the structs we just defined. It matches and assigns variables if a particular user type and theme is detected:

fn destructuring_match(app: MyApp) -> String {
match app {
MyApp { user_type: UserType::Power,
secret_user_id: uid,
theme: Background::Color(b1, b2, b3) } =>
format!("A power user with id >{}< and color background
(#{:02x}{:02x}{:02x})", uid, b1, b2, b3),
MyApp { user_type: UserType::Power,
secret_user_id: uid,
theme: Background::Image(path) } =>
format!("A power user with id >{}< and image background
(path: {})", uid, path),
MyApp { user_type: _, secret_user_id: uid, .. } => format!
("A regular user with id >{}<, individual backgrounds not
supported", uid),
}
}
  1. On top of the powerful regular matching, a guard can also enforce certain conditions. Similar to destructuring, we can add more constraints:
fn guarded_match(app: MyApp) -> String { 
match app {
MyApp { secret_user_id: uid, .. } if uid <= 100 => "You are
an early bird!".to_owned(),
MyApp { .. } => "Thank you for also joining".to_owned()
}
}
  1. So far, borrowing and ownership has not been a significant concern. However, the match clauses so far have all taken ownership and transferred it to the scope of the matching arm (anything after =>), which, unless you return it, means that the outside scope cannot do any other work with it. To remedy that, references can be matched as well:
fn reference_match(m: &Option<&str>) -> String {
match m {
Some(ref s) => s.to_string(),
_ => "Nothing".to_string()
}
}
  1. In order to go full circle, we have not yet matched a particular type of literal: the string literal. Due to their heap allocation, they are fundamentally different from types such as i32 or usize. Syntactically, however, they don't look different from any other form of matching:
  1. Now, let's tie it all together and build a main function that calls the various functions with the right parameters. Let's begin by printing a few simpler matches:
pub fn main() {
let opt = Some(42);
match opt {
Some(nr) => println!("Got {}", nr),
_ => println!("Found None")
}
println!();
println!("Literal match for 0: {}", literal_match(0));
println!("Literal match for 10: {}", literal_match(10));
println!("Literal match for 100: {}", literal_match(100));

println!();
println!("Literal match for 0: {}", tuple_match((0, 10, 0,
100)));

println!();
let mystr = Some("Hello");
println!("Matching on a reference: {}",
reference_match(&mystr));
println!("It's still owned here: {:?}", mystr);

Next, we can also print the destructured matches:

    println!();
let power = MyApp {
secret_user_id: 99,
theme: Background::Color(255, 255, 0),
user_type: UserType::Power
};
println!("Destructuring a power user: {}",
destructuring_match(power));

let casual = MyApp {
secret_user_id: 10,
theme: Background::Image("my/fav/image.png"),
user_type: UserType::Casual
};
println!("Destructuring a casual user: {}",
destructuring_match(casual));

let power2 = MyApp {
secret_user_id: 150,
theme: Background::Image("a/great/landscape.png"),
user_type: UserType::Power
};
println!("Destructuring another power user: {}",
destructuring_match(power2));

And lastly, let's see about guards and literal string matches on UTF symbols:

  1. The last step again involves running the program. Since this is not a library project, the results will be printed on the command line. Feel free to change any of the variables in the main function to see how it affects the output. Here's what the output should be:

Now, let's take a peek behind the scenes to understand the code better.