Defining an Enum

Enums give us a way of saying a value is one of a possible set of values.

enum IpAddrKind {
    V4, // if not specified, default to be 0, and
    V6, // each following item will be the next num
}

// creation will look like this
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

// we can also put data directly inside enum as well
enum IpAddr {
    V4(String),
    V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));

// each variant of enum can have different types and 
// amounts of associated data
enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

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

We can put any kind of data inside an enum variant: strings, numeric types, structs, or even another enum.

The Option Enum and Its Advantages Over Null Values

The Option type encodes the very common scenario in which a value could be something or it could be nothing. Since Rust does not have null, we use enum of Option<T> that can encode the concept of a value being present or absent.

enum Option<T> {
    None,
    Some(T),
}

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, it means the same thing as null in some sense: we don’t have a valid value.

To use the value inside an Option<T>, we need to convert the Option<T> to T first. In general, in order to use an Option<T> value, you want to have code that will handle each variant. There need to be some code that will run only when you have a Some<T> value which is allowed to use the inner T. There also need to be some code to run only if you have a None value which won’t have the T available.

The match expression is a control flow construct that does just this when used with enums: it will run different code depending on which variant of the enum it has, and that code can use the data inside the matching value.

The match Control Flow Construct

match is used to compare a value against a series of patterns and then execute code based on which pattern matches. Patterns can be made up of literal values, variable names, wildcards, and many other thins. When match is used, compiler would confirm that all possible cases are handled.

enum UsState {
    Alabama,
    Alaska,
    ...
}

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

if multiple lines of code needed to be run in a match arm, curly brackets are used. Inside an arm, we can also access the parts of the value that match the pattern(in the above example, we can access state in the Coin enum)

To handle a default case, we could simply do this:

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    other => move_player(other),
		// if we don't want to use the value, we can do
		// _ => do_something(),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}

Concise Control Flow with if let

We can rewrite this following code

let config_max = Some(3u8);
match config_max {
    Some(max) => println!("The maximum is configured to be {}", max),
    _ => (),
}

to this using if let