Frontend Labs
⋯

9. Structs, enums, and pattern matching

9.1 Structs and impl blocks

#[derive(Debug, Clone, PartialEq)]
struct Endpoint {
    host: String,
    port: u16,
    tls: bool,
}

impl Endpoint {
    // An associated function (no `self`): Rust's constructor convention is `new`.
    fn new(host: impl Into<String>, port: u16) -> Self {
        Self { host: host.into(), port, tls: port == 443 }
    }
    // &self: read-only method. &mut self: mutating. self: consuming.
    fn url(&self) -> String {
        let scheme = if self.tls { "https" } else { "http" };
        format!("{scheme}://{}:{}", self.host, self.port)
    }
    fn with_tls(mut self) -> Self { self.tls = true; self }
}

struct Meters(f64);        // tuple struct: the NEWTYPE pattern (Ch 53, 62)
struct Marker;             // unit struct: zero bytes

fn main() {
    let e = Endpoint::new("example.com", 443);
    let local = Endpoint { host: "localhost".into(), ..e.clone() }; // struct update syntax
    let dev = Endpoint::new("dev.local", 8443).with_tls();
    println!("{} {} {}", e.url(), local.url(), dev.url());
    let Meters(m) = Meters(3.5);
    println!("{m} {}", std::mem::size_of::<Marker>());
}

Receiver choice is the Rust version of Go's value vs. pointer receiver rule, with a third option. self by value consumes the struct, which builders and state machines use.

9.2 Enums are sum types

A Rust enum is a tagged union, the C guide's §8.3 pattern, except the compiler checks the tag for you:

#[derive(Debug)]
enum Message {
    Ping,                                   // no data
    Data { stream: u32, payload: Vec<u8> }, // named fields
    Close(u16, String),                     // tuple-like
}

fn handle(m: &Message) -> String {
    match m {
        Message::Ping => "pong".into(),
        Message::Data { stream, payload } if payload.is_empty() => format!("empty frame on {stream}"),
        Message::Data { stream, payload } => format!("{} bytes on {stream}", payload.len()),
        Message::Close(code, reason) => format!("close {code}: {reason}"),
    }
    // Remove any arm and you get error[E0004]: non-exhaustive patterns.
}

fn main() {
    let msgs = [
        Message::Ping,
        Message::Data { stream: 3, payload: vec![1, 2, 3] },
        Message::Data { stream: 5, payload: vec![] },
        Message::Close(1000, "bye".into()),
    ];
    for m in &msgs { println!("{}", handle(m)); }
}

Exhaustiveness is the main benefit. When you add a Message::Resume variant, every match that does not handle it stops compiling, so the compiler gives you the list of places to update.

9.3 Option<T> instead of null

fn find_user(id: u32) -> Option<&'static str> {
    match id {
        1 => Some("ada"),
        2 => Some("grace"),
        _ => None,
    }
}

fn main() {
    // You cannot use an Option<&str> as a &str. Handle the None case first.
    let name = find_user(3).unwrap_or("anonymous");
    let upper = find_user(1).map(|n| n.to_uppercase());       // Some("ADA")
    let len = find_user(2).map_or(0, |n| n.len());            // 5
    let both = find_user(1).zip(find_user(2));                // Some(("ada","grace"))
    println!("{name} {upper:?} {len} {both:?}");

    // Option<&T> costs no extra space: the null pointer is used as the None "niche".
    println!("{}", std::mem::size_of::<Option<&u64>>()); // 8, same as &u64 (Ch 57)
}

Tony Hoare called null his "billion-dollar mistake". Go's nil-pointer panic and C's NULL dereference are two versions of it. Option costs nothing at runtime for references and boxes. The only cost is that you have to handle None before you use the value.

9.4 Patterns everywhere

fn main() {
    let packet = (4u8, [192u8, 168, 1, 10], 443u16);
    match packet {
        (4, [10, ..], _) => println!("private 10/8"),
        (4, [192, 168, ..], port @ (80 | 443)) => println!("LAN web on {port}"),
        (4, [a, b, c, d], p) => println!("{a}.{b}.{c}.{d}:{p}"),
        (v, ..) => println!("IPv{v}"),
    }

    let point = (3, -3);
    if let (x, y) = point && x == -y {          // let-chains: edition 2024
        println!("on the anti-diagonal");
    }
}

.. skips the remaining elements, @ binds a value while testing it, | matches alternatives, and if adds a guard. Patterns also work in let, function parameters, for, and closures.

Real-world example. HTTP routers, protocol state machines, and compilers' AST passes are where Rust's enums are most useful. Ch 63's RESP parser is one enum Frame plus a match.


Generated from real-life-rust-guide.md · 8 Oct 2026 16:36