6. Enums and Pattern Matching

Enums (enumerations) allow you to define a type by enumerating its possible variants.

Enums are a feature in many languages, but their capabilities differ in each language.

Rust's enums are most similar to algebraic data types in functional languages, such as F#, OCaml, and Haskell.

6.1 Enum Definition & Differences with

fn main() {
    enum IpAddrKind {
        V4,
        V6,
    }

    enum IpAddr {
        V4(u8, u8, u8, u8),
        V6(String),
    }

    let home = IpAddr::V4(127, 0, 0, 1);
    let loopback = IpAddr::V6(String::from("::1"));

    enum Message {
        Quit,
        Move { x: i32, y: i32 },
        Write(String),
        ChangeColor(i32, i32, i32),
    }

    impl Message {
        fn call(&self) {
            // method body would be defined here
        }
    }

    let m = Message::Write(String::from("hello"));
    m.call();
}
  • We can attach data to each variant of the enum directly.
  • Each variant can have different types and amounts of associated data.
  • We can define methods on enums.

6.2 The Option Enum V.S. Null Values

Option is another enum defined by the standard library.

Null: billion-dollar mistake. Rust does not have nulls, but it does have an enum that can encode the concept of a value being present or absent. This enum is Option, and it is defined by the standard library as follows:

#![allow(unused)]
fn main() {
    enum Option<T> {
        None,
        Some(T),
    }
}

The Option<T> enum is so useful that it's even included in the prelude; you don't need to bring it into scope explicitly. In addition, so are its variants: you can use Some and None directly without the Option:: prefix. The <T> syntax: generic type parameter. <T> means the Some variant of the Option enum can hold one piece of data of any type. Here are some examples of using Option values to hold number types and string types:

fn main() {
    let some_number = Some(5);
    let some_string = Some("a string");

    let absent_number: Option<i32> = None;
    // if we use None rather than Some,
    // we need to tell Rust what type of Option<T> we have
    // because the compiler can't infer the type,
    // that the `Some` variant will hold by looking only at a `None` value.
}

When we have a Some value, we know that a value is present and the value is held within the Some. When we have a None value, in some sense, it means the same thing as null: we don't have a valid value. We can proceed confidently without having to check for null before using that value. You have to convert an Option<T> to a T before you can perform T operations with it. Generally, this helps catch one of the most common issues with null: assuming that something isn't null when it actually is.

6.3 The match Control Flow Operator

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
        // pattern => code
        // the => operator that separates the pattern and the code (expression) to run
        // arms are executed in order
    }
}

fn main() {}

6.4 Patterns that Bind to Values

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

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
        }
    }
}

fn main() {
    value_in_cents(Coin::Quarter(UsState::Alaska));
}

6.5 Matching with Option<T>

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

Matches in Rust are exhaustive: we must exhaust every last possibility in order for the code to be valid.

6.6 The _ Placeholder

fn main() {
    let some_u8_value = 0u8;
    match some_u8_value {
        1 => println!("one"),
        3 => println!("three"),
        5 => println!("five"),
        7 => println!("seven"),
        _ => (),
        // () is the unit value
        // so nothing will happen in the _ case
    }
}

The _ pattern will match any value. By putting it after our other arms, the _ will match all the possible cases that aren't specified before it.

6.7 if let Concise Control Flow

The if let syntax lets you combine if and let into a less verbose way to handle values that match one pattern while ignoring the rest.

fn main() {
    let some_u8_value = Some(0u8);
    match some_u8_value {
        Some(3) => println!("three"),
        _ => (),
    }
}

The following code behaves the same:

fn main() {
    let some_u8_value = Some(0u8);
    if let Some(3) = some_u8_value {
        println!("three");
    }
}

The syntax if let takes a pattern and an expression separated by an equal sign. It works the same way as a match, where the expression is given to the match and the pattern is its first arm.

Using if let means less typing, less indentation, and less boilerplate code. However, you lose the exhaustive checking that match enforces. Choosing between match and if let depends on what you’re doing in your particular situation and whether gaining conciseness is an appropriate trade-off for losing exhaustive checking.

In other words, you can think of if let as syntax sugar for a match that runs code when the value matches one pattern and then ignores all other values.

We can include an else with an if let.

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {
    let coin = Coin::Penny;
    let mut count = 0;
    match coin {
        Coin::Quarter(state) => println!("State quarter from {:?}!", state),
        _ => count += 1,
    }
}

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {
    let coin = Coin::Penny;
    let mut count = 0;
    if let Coin::Quarter(state) = coin {
        println!("State quarter from {:?}!", state);
    } else {
        count += 1;
    }
}

They are the same.