Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.syntblaze.com/llms.txt

Use this file to discover all available pages before exploring further.

An enum (enumeration) in Rust is an algebraic data type—specifically a sum type or tagged union—that allows a value to be exactly one of a predefined set of mutually exclusive variants. Unlike enums in languages like C or Java, Rust enums can encapsulate heterogeneous data payloads directly within their variants, making them highly expressive for state representation and type-safe data encapsulation.

Variant Types and Type Identity

Rust supports three distinct types of enum variants, which can be mixed within a single enum declaration:
  1. Unit-like variants: Contain no data.
  2. Tuple-like variants: Contain unnamed, ordered fields.
  3. Struct-like variants: Contain named fields.
enum Message {
    Quit,                       // Unit-like
    Write(String),              // Tuple-like (single field)
    ChangeColor(u8, u8, u8),    // Tuple-like (multiple fields)
    Move { x: i32, y: i32 },    // Struct-like
}
A critical distinction in Rust is that enum variants are not independent types. The type of any instantiated variant is always the parent enum (Message). A function cannot accept a specific variant (e.g., Message::Write) as a parameter type or variable type signature; it must accept the parent Message type.

Instantiation

Instances of an enum are created by scoping the variant under the enum’s namespace using the path separator (::). The syntax for instantiation mirrors the syntax of the variant’s declaration.
enum Message {
    Quit,
    Write(String),
    Move { x: i32, y: i32 },
}

let unit_val = Message::Quit;
let tuple_val = Message::Write(String::from("Payload"));
let struct_val = Message::Move { x: 10, y: 20 };

Methods and Associated Functions

Enums in Rust possess object-like capabilities. You can define methods and associated functions on them using impl blocks, allowing behavior to be tightly coupled with the algebraic data type.
enum Message {
    Quit,
    Write(String),
}

impl Message {
    // Associated function (constructor)
    fn new_write(text: &str) -> Self {
        Message::Write(text.to_string())
    }

    // Method
    fn print_debug(&self) {
        match self {
            Message::Quit => println!("Quitting"),
            Message::Write(text) => println!("Writing: {}", text),
        }
    }
}

let msg = Message::new_write("Hello");
msg.print_debug();

Pattern Matching and Destructuring

Because enum variants can hold data, accessing that data requires pattern matching. Rust enforces exhaustiveness checking, meaning every possible variant must be accounted for at compile time. Data is extracted from variants via destructuring in match expressions or if let bindings. When matching on an enum that contains non-Copy data (like String), the match expression will consume the value unless it is borrowed.
enum Message {
    Quit,
    Write(String),
    ChangeColor(u8, u8, u8),
    Move { x: i32, y: i32 },
}

let msg = Message::Write(String::from("Hello"));

// Borrowing `msg` prevents consuming the non-Copy String payload
match &msg {
    Message::Quit => {
        // Handle unit variant
    }
    Message::Write(text) => {
        // `text` is bound to `&String`
    }
    Message::ChangeColor(r, g, b) => {
        // `r`, `g`, and `b` are bound to `&u8`
    }
    Message::Move { x, y } => {
        // `x` and `y` are bound to `&i32`
    }
}
If only a single variant is of interest, if let provides a more concise syntax for destructuring without requiring exhaustive match arms. Because msg was borrowed in the previous example, it remains valid for subsequent use:
// `msg` is still valid here
if let Message::Write(text) = &msg {
    // Executes only if `msg` is the `Write` variant
}

Memory Layout

Under the hood, a Rust enum is typically implemented as a tagged union. The compiler allocates memory based on two components:
  1. The Discriminant (Tag): A hidden integer used by the compiler to track which variant is currently active.
  2. The Payload: The actual data contained within the variant.
By default, the total size of an enum in memory is determined by the size of its largest variant, plus the size of the discriminant, plus any necessary padding for memory alignment.
enum Data {
    Empty,          // 0 bytes payload
    Small(u8),      // 1 byte payload
    Large([u64; 4]) // 32 bytes payload
}
// The size of `Data` will be at least 32 bytes + discriminant size + padding.
Niche Optimization Rust employs a highly efficient memory layout strategy known as niche optimization (often referred to as null-pointer optimization). If a variant’s payload contains “niches”—invalid bit patterns for that specific type (e.g., a null pointer for a reference &T, or the value 0 for std::num::NonZeroU8)—the compiler can store the enum’s discriminant inside those invalid bit patterns. This eliminates the need for a separate discriminant in memory. Because of this optimization, an enum like Option<&T> has the exact same memory footprint as a raw pointer &T, with zero extra bytes added for the enum tag.

Explicit Discriminants

For unit-only enums (often called C-like enums), you can explicitly define the discriminant values. This is primarily used for FFI (Foreign Function Interface) or when casting to primitive integer types. You can control the memory representation using the #[repr] attribute. The chosen representation must be large enough to hold the assigned values.
#[repr(u16)]
enum HttpError {
    BadRequest = 400,
    Unauthorized = 401,
    Forbidden = 403,
}

// Casting to the underlying primitive type
let code = HttpError::BadRequest as u16; // 400

Generic Enums

Enums can be parameterized over types and lifetimes. This allows the definition of abstract sum types that operate on arbitrary data.
enum Result<T, E> {
    Ok(T),
    Err(E),
}
Master Rust with Deep Grasping Methodology!Learn More