Skip to Content
RustBooksThe Rust BookCh 6. Enum and Pattern Matching

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 u8 values and IPv6 addresses as a String:

    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 Some and None directly without needing to import Option.
  • 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_number is of type Option<i32>, and some_char is Option<char>.
    • absent_number is explicitly declared as Option<i32>, with the value None.

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> and T are distinct types, Rust prevents you from using them interchangeably.

  • You must explicitly handle the None case and extract the value from Some(T).

Handling Option<T>

  • To work with Option<T>, you need to handle both Some and None cases.
  • The match expression is commonly used to handle different variants of Option and 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

  • match is 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 match highly 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 of coin against 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 match expression.
    • For more complex actions, match arms can include multiple lines of code, enclosed in curly brackets {}.

Patterns that Bind to Values

  • match patterns can bind to values, allowing you to extract data from an enum variant.

  • Example: Changing the Coin::Quarter variant to hold a UsState value (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 match arm Coin::Quarter(state) binds state to the UsState value inside Coin::Quarter.
    • You can then use state in the match arm’s code, such as printing the state name.

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 Some variant 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

  1. Pattern Matching: Allows matching on specific patterns (like Some) while ignoring other cases.

  2. Less Verbose: Reduces boilerplate by eliminating the need to handle unused cases, making code shorter and cleaner.

  3. Optional else: You can pair if let with else to 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 let is more concise, but it sacrifices the exhaustive checking enforced by match. This means you won’t get compile-time errors if new variants are added to an enum and not handled.
  • Use case

    • Use if let when you’re only interested in one pattern and want simpler, cleaner code.
    • Use match when you need to handle multiple patterns or want to enforce exhaustiveness for safety.
Last updated on