Enums

  • An enum is a single type that contains variants, which represent the possible values of the enum at any given time.
  • By convention, the enum name and its variants’ names should follow PascalCase.
  • Can access the variants using the :: notation and the variant name. ex. Day::Sunday
enum Day {
    Sunday,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
}

// πŸ’‘ Day is the enum. Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday are its variants.
  • An enum variant can have either,
    • No data (a unit variant)
    • Unnamed ordered data (a tuple variant)
    • Named data/ fields (a struct variant)
    enum FlashMessage {
        Success, // πŸ’‘ A unit variant (no data)
        Error(u8, String), // πŸ’‘ A tuple variant (one or more , separated data)
        Warning { field: String, message: String }, // πŸ’‘ A struct variant (one or more , separated name: value data)
    }
    
    // πŸ’‘ FlashMessage is the emnum, Success, Error, Warning are its variants.
    

πŸ’‘ In Rust, the term “instantiation” is used to describe the act of creating a concrete instance of a type (struct or enum).

πŸ’‘ In Rust, the term “field” is used to describe a named component in a C-like struct & struct-like enum variant, and the term “element” is used to describe an unnamed component in a tuple struct & tuple-like enum variant. The term “member” is used to describe both.

πŸ’― More complex examples can be found on Generics, Impls and Traits, Lifetimes and Modules sections.

Instantiation

#![allow(unused)] // πŸ’‘ skip unused warnings, as we don't read fields in the enums

#[derive(Debug)]
enum FlashMessage { // Definition
    Success,
    Error(u32, String),
    Warning { field: String, message: String },
}

fn main() {
    // 1. Instantiation with separate variable declaration and assignment
    let x: FlashMessage; // Declaration with the data type
    x = FlashMessage::Success;
    println!("{x:?}"); // Success
    
    // 2. Instantiation with a direct variable initialization
    let a = FlashMessage::Success;
    let b = FlashMessage::Error(401, "Unauthorized".to_string());
    let c = FlashMessage::Warning { field: "email".to_string(), message: "This is required".to_string() };

    println!("{a:?}"); // Success
    println!("{b:?}"); // Error(401, "Unauthorized")
    println!("{c:?}"); // Warning { field: "email", message: "This is required" }    
}
// 3. Instantiation with a default variant
#![allow(unused)] // πŸ’‘ skip unused warnings, as we don't use the all variants of the enum

#[derive(Debug, Default)]
enum Hand {
    Left,
    #[default] // πŸ’‘Set Right as the default variant
    Right,
}

fn main() {
    let a = Hand::default(); // Instantiation with the default variant
    println!("{a:?}"); // Right
}

In Rust, the #[derive()] attribute is used to automatically generate an implementation of certain traits for a custom data structure (struct and enum), instead of you writing them by hand. The std::fmt::Debug trait allows us to format a value with {:?} or {:#?} in println! and similar macros. The std::default::Default trait allows us to create a new instance of a type with the Type::default() method.

Pattern Matching

With match

#![allow(unused)] // πŸ’‘ skip unused warnings, as we don't use the all variants of the enum

enum Season {
    Spring,
    Summer,
    Autumn,
    Winter,
}

fn main() {
    let a = Season::Winter;
    let result = match a {
        Season::Spring => "β˜€οΈ",
        Season::Summer => "🍁",
        Season::Autumn => "πŸ‚",
        Season::Winter => "❄️",
    };

    println!("{result}"); // ❄️
}

With if let, else if let, else

if let is useful when we only care about handling one (or few) specific patterns and don’t need to explicitly match every possible case.

#![allow(unused)] // πŸ’‘ skip unused warnings, as we don't use the all variants of the enum

enum Season {
    Spring,
    Summer,
    Autumn,
    Winter,
}

fn main() {
    let a = Season::Winter;
    let result = if let Season::Spring = a {
        "β˜€οΈ"
    } else if let Season::Summer = a {
        "🍁"
    } else if let Season::Autumn = a {
        "πŸ‚"
    } else if let Season::Winter = a {
        "❄️"
    } else {
        unreachable!()
    };

    println!("{result}"); // ❄️
}

Destructuring & Accessing Variants’ Members

In Rust, directly accessing an enum variant’s fields without any form of pattern matching is not possible. We need to use pattern matching to access the fields by using a match expression or if let expression.

With match

#![allow(unused)] // πŸ’‘ skip unused warnings, as we don't use the all variants of the enum

enum FlashMessage {
    Success,
    Error(u32, String),
    Warning { field: String, message: String },
}

fn main() {
    let a = FlashMessage::Error(401, "Unauthorized".to_string());

    let result = match a {
        FlashMessage::Success => "We'll get back to you.".to_string(),
        FlashMessage::Error(_, msg) => msg, // πŸ’‘ Destructuring only the second element of the tuple variant.
        FlashMessage::Warning { message, .. } => message, // πŸ’‘ Destructuring only the second field of the struct variant.
    };

    println!("{result}"); // Unauthorized
}

With if let, else if let, else

if let is useful when we only care about handling one (or few) specific patterns and don’t need to explicitly match every possible case.

#![allow(dead_code)] // πŸ’‘ Remove dead_code warnings, as we don't access the all elements of variants.

enum FlashMessage {
    Success,
    Error(u32, String),
    Warning { field: String, message: String },
}

fn main() {
    let a = FlashMessage::Error(401, "Unauthorized".to_string());

    if let FlashMessage::Error(_, msg) = a {
        println!("{msg}"); // Unauthorized
    } else if let FlashMessage::Warning { message, .. } = a {
        println!("{message}");
    } else {
        println!("We'll get back to you.");
    }
}