Rust Enums & Pattern Matching

Basic Enums

C-style enums and data-carrying variants
// C-style — no data
enum Direction {
    North,
    South,
    East,
    West,
}

// Data-carrying variants — each can hold different types
enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

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

Rust enums are algebraic data types — each variant can carry different data. This makes them vastly more powerful than C/Go enums.

Pattern Matching with match

match is exhaustive — every variant must be handled
fn describe(addr: &IpAddr) -> String {
    match addr {
        IpAddr::V4(a, b, c, d) => format!("{a}.{b}.{c}.{d}"),
        IpAddr::V6(s) => s.clone(),
    }
}

// Catch-all with _ or variable
fn priority(code: u16) -> &'static str {
    match code {
        200 => "OK",
        404 => "Not Found",
        500 => "Server Error",
        _ => "Unknown",
    }
}

match is exhaustive — the compiler forces you to handle every variant. _ is the catch-all. Forgetting a variant is a compile error, not a runtime bug.

Option<T>

Rust’s replacement for null
fn find_host(name: &str) -> Option<String> {
    match name {
        "sw01" => Some(String::from("10.50.1.10")),
        "fw01" => Some(String::from("10.50.1.1")),
        _ => None,
    }
}

// Handling Option
match find_host("sw01") {
    Some(ip) => println!("Found: {ip}"),
    None => println!("Not found"),
}

// Combinators — avoid nested match
let ip = find_host("sw01").unwrap_or("0.0.0.0".into());
let upper = find_host("sw01").map(|ip| ip.to_uppercase());
let ip = find_host("sw01").unwrap_or_else(|| default_host());

// if let — when you only care about Some
if let Some(ip) = find_host("sw01") {
    println!("Found: {ip}");
}

Option<T> replaces null. Some(value) means present, None means absent. The compiler forces you to handle both cases.

Result<T, E>

Success or error — no exceptions
use std::fs;
use std::io;

fn read_config(path: &str) -> Result<String, io::Error> {
    fs::read_to_string(path)
}

match read_config("/etc/app.conf") {
    Ok(contents) => println!("{contents}"),
    Err(e) => eprintln!("Error: {e}"),
}

Result<T, E> is how Rust handles errors. Ok(value) for success, Err(error) for failure. No exceptions, no hidden control flow.

Enum Methods

impl blocks on enums
enum Command {
    Ping { host: String },
    Scan { host: String, ports: Vec<u16> },
    Quit,
}

impl Command {
    fn execute(&self) {
        match self {
            Command::Ping { host } => {
                println!("pinging {host}...");
            }
            Command::Scan { host, ports } => {
                println!("scanning {host} on {:?}", ports);
            }
            Command::Quit => {
                println!("exiting");
            }
        }
    }
}

Guards and Bindings

Advanced pattern matching
let num = 42;

match num {
    n if n < 0 => println!("negative"),
    0 => println!("zero"),
    1..=10 => println!("small"),
    n @ 11..=100 => println!("medium: {n}"),
    _ => println!("large"),
}

// Destructuring nested enums
enum Packet {
    Data { payload: Vec<u8>, port: u16 },
    Ack(u32),
    Error(String),
}

match packet {
    Packet::Data { port: 22, .. } => println!("SSH data"),
    Packet::Data { port, payload } => println!("port {port}: {} bytes", payload.len()),
    Packet::Ack(seq) => println!("ack {seq}"),
    Packet::Error(msg) => eprintln!("error: {msg}"),
}

Match guards (if condition) add runtime checks. @ binds a matched value to a name. .. ignores remaining fields.