Ch 6. Enum and Pattern Matching
6.1 Defining an Enum
-
Enums allow you to define a type that can be one of several variants.
-
Unlike structs, which group together related fields, enums allow a value to be one of several predefined types, called variants.
-
Example: For IP addresses, an address can be either IPv4 or IPv6.
enum IpAddrKind { V4, V6, } // Creating Enum Instances let four = IpAddrKind::V4; let six = IpAddrKind::V6; // pass these enum variants to functions: fn route(ip_kind: IpAddrKind) {} route(IpAddrKind::V4); route(IpAddrKind::V6); // Combining Enums with Structs struct IpAddr { kind: IpAddrKind, address: String, } let home = IpAddr { kind: IpAddrKind::V4, address: String::from("127.0.0.1"), }; let loopback = IpAddr { kind: IpAddrKind::V6, address: String::from("::1"), };// You can attach data directly to the enum variants // making structs unnecessary. enum IpAddr { V4(String), V6(String), } let home = IpAddr::V4(String::from("127.0.0.1")); let loopback = IpAddr::V6(String::from("::1"));
Storing Different Types of Data in Variants
-
Enums can hold different types and amounts of data in each variant.
-
Example: Storing IPv4 addresses as four
u8values and IPv6 addresses as aString:enum IpAddr { V4(u8, u8, u8, u8), V6(String), } let home = IpAddr::V4(127, 0, 0, 1); let loopback = IpAddr::V6(String::from("::1"));
A More Complex Enum Example
-
Enums can store various types and amounts of data in different variants.
enum Message { Quit, // No data Move { x: i32, y: i32 }, // Struct-like fields Write(String), // Tuple struct ChangeColor(i32, i32, i32), // Tuple struct with multiple values } -
You can represent the same data using structs:
struct QuitMessage; // unit struct struct MoveMessage { x: i32, y: i32 } struct WriteMessage(String); // tuple struct struct ChangeColorMessage(i32, i32, i32); // tuple struct -
However, enums group related variants under one type, making it easier to handle multiple cases in a single function or method.
Defining Methods on Enums
-
Like structs, enums can have methods using
impl.impl Message { fn call(&self) { // Method body here } } let m = Message::Write(String::from("hello")); m.call();
The Option Enum
-
Option<T>is an enum that represents the concept of a value being either present (Some) or absent (None).- It’s included in Rust’s prelude, so you can use
SomeandNonedirectly without needing to importOption.
- It’s included in Rust’s prelude, so you can use
-
This allows the type system to handle scenarios where a value may or may not exist, which improves safety compared to null values in other languages.
-
The
Option<T>enum is defined in the standard library as:enum Option<T> { None, Some(T), } //Example of storing numbers and characters using `Option`: let some_number = Some(5); let some_char = Some('e'); let absent_number: Option<i32> = None; -
Explanation:
some_numberis of typeOption<i32>, andsome_charisOption<char>.absent_numberis explicitly declared asOption<i32>, with the valueNone.
Why Option<T> Is Better Than Null
-
Null values can cause errors when a null value is used where a non-null value is expected, leading to runtime crashes.
-
In contrast,
Option<T>ensures that the absence of a value is handled explicitly in the type system.let x: i8 = 5; let y: Option<i8> = Some(5); let sum = x + y; // This won't compile! // error[E0277]: cannot add `Option<i8>` to `i8` -
Since
Option<T>andTare distinct types, Rust prevents you from using them interchangeably. -
You must explicitly handle the
Nonecase and extract the value fromSome(T).
Handling Option<T>
- To work with
Option<T>, you need to handle bothSomeandNonecases. - The
matchexpression is commonly used to handle different variants ofOptionand ensures that the absence of a value (None) is handled safely.
fn main() {
let some_number = Some(5);
let absent_number: Option<i32> = None;
match some_number {
Some(value) => println!("We have a value: {}", value),
None => println!("No value present"),
}
match absent_number {
Some(value) => println!("We have a value: {}", value),
None => println!("No value present"),
}
}6.2 The match Control Flow Construct
-
matchis a powerful control flow construct in Rust that compares a value against a series of patterns and executes code based on which pattern matches. -
Patterns can be literals, variable names, wildcards, and more, making
matchhighly flexible. -
The compiler ensures that all possible cases are handled.
enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => { println!("Lucky penny!"); 1 } Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } }match coin { ... }checks the value ofcoinagainst each arm.- Each arm has a pattern (
Coin::Penny,Coin::Nickel, etc.) and associated code (1,5,10,25). - The first pattern that matches executes its associated code, similar to a coin-sorting machine.
- The value returned from the matching arm is the result of the entire
matchexpression. - For more complex actions, match arms can include multiple lines of code, enclosed in curly brackets
{}.
Patterns that Bind to Values
-
matchpatterns can bind to values, allowing you to extract data from an enum variant. -
Example: Changing the
Coin::Quartervariant to hold aUsStatevalue (Listing 6-4):#[derive(Debug)] enum UsState { Alabama, Alaska, // More states... } enum Coin { Penny, Nickel, Dime, Quarter(UsState), fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter(state) => { println!("State quarter from {state:?}!"); 25 } } } // Calling the function value_in_cents(Coin::Quarter(UsState::Alaska)); // Prints: "State quarter from Alaska!"- The
matcharmCoin::Quarter(state)bindsstateto theUsStatevalue insideCoin::Quarter. - You can then use
statein the match arm’s code, such as printing the state name.
- The
6.3 Concise Control Flow with if let
The if let syntax in Rust provides a concise way to match on specific patterns without requiring exhaustive matching, making it less verbose than a traditional match expression.
match syntax:
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {max}"),
_ => (),
}- Here, the code only handles the
Somevariant but requires a default case (_ => ()) for completeness, adding unnecessary boilerplate.
if let syntax:
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {max}");
}- This does the same thing with less typing, less indentation, and avoids the need for an explicit
_case.
Key Features of if let
-
Pattern Matching: Allows matching on specific patterns (like
Some) while ignoring other cases. -
Less Verbose: Reduces boilerplate by eliminating the need to handle unused cases, making code shorter and cleaner.
-
Optional
else: You can pairif letwithelseto handle cases that don’t match the pattern:if let Some(max) = config_max { println!("Configured to be {max}"); } else { println!("No configuration found"); }
-
Trade-off: Conciseness
if letis more concise, but it sacrifices the exhaustive checking enforced bymatch. This means you won’t get compile-time errors if new variants are added to an enum and not handled.
-
Use case
- Use
if letwhen you’re only interested in one pattern and want simpler, cleaner code. - Use
matchwhen you need to handle multiple patterns or want to enforce exhaustiveness for safety.
- Use