Rust Interview Mastery · Chapter 06 06

Enums and Pattern Matching

Algebraic data types, Option, and match — the control-flow tool that makes the type system usable.

9 questions 3 topics Intermediate

Defining & Modeling with Enums

Enum vs struct — what's the fundamental difference in what they say about data?

A struct says a value has all of these fields at once. An enum says a value is exactly one of these variants:

enum Message {
    Quit,
    Move { x: i32, y: i32 },   // struct-like fields
    Write(String),           // tuple-like payload
    ChangeColor(i32, i32, i32),
}

Each variant can carry different data, and variants can even have methods via impl. This is an algebraic data type (sum type) — the thing structs alone can't express. It's how Rust models "one of several shapes" — network events, parser tokens, states.

How does the book model IpAddr with enums instead of structs, and why is it better?
enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);

A struct version forces every address to carry both a kind tag and fields for both versions — including data that makes no sense for the variant. The enum embeds the tag and the variant-specific payload in one type: a V4 is exactly four bytes of data, a V6 is a String. The compiler then guarantees you handle both cases wherever you match.

Can enums have methods?

Yes — same impl syntax as structs:

impl Message {
    fn call(&self) { /* dispatch on self */ }
}
let m = Message::Write(String::from("hi"));
m.call();

Inside the method you match self to extract the payload. This keeps variant-handling logic beside the type definition instead of scattered across if let chains.

Option — Rust's Null Replacement

Why does Rust have Option<T> instead of null, and what did it kill?

Null's billion-dollar problem: every reference might be null but the type doesn't say so — you must remember to check, and forgetting crashes at runtime. Rust deletes the concept: a String is never absent, and a function that might not produce a value returns Option<String>.

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

Now "might be absent" is in the type, and you can't touch the T without handling None — via match, if let, unwrap, map, etc. The null-check moves from programmer discipline to compiler enforcement. ("Additional context:" Option is so universal it's in the prelude — no import needed.)

Show the common ways to consume an Option and when each is right.
match opt { Some(v) => use(v), None => fallback() }  // full control
if let Some(v) = opt { use(v) }                     // only care about Some
let v = opt.unwrap_or(default);                     // fallback value
let v = opt.unwrap_or_else(compute_default);        // lazy fallback
let n = opt.map(|x| x * 2);                         // transform, stay Option
let v = opt.unwrap();                               // panic on None — tests only

Rule of thumb: match/if let when logic differs per case, unwrap_or* for defaults, combinators (map/and_then/filter) for pipelines, unwrap/expect when None is provably impossible or in tests.

Why can't you just add a number to an Option<i32>?

Because Option<i32> and i32 are different types — the + operator isn't defined across them. That's the point: the compiler forces the question "what should happen when it's None?" before arithmetic can proceed.

let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y.unwrap_or(0);   // explicit handling required

In a null language x + y compiles and explodes at runtime. Rust moves the explosion to compile time — and makes you write what you actually meant.

match, if let & Control Flow

What makes match more powerful than a switch statement?

Three things. Binding: arms destructure and bind — Some(v) => v * 2 pulls the value out. Exhaustiveness: every possible value needs an arm or the code doesn't compile — no silent fallthrough bugs. Expression: match returns a value — let x = match c { ... };.

match coin {
    Coin::Penny => 1,
    Coin::Nickel => 5,
    Coin::Quarter(state) => { println!("{state}"); 25 }  // binds data
}

Plus the _ catch-all for "everything else" when you genuinely don't care.

When should you use if let instead of match?

When you only care about one pattern and a full match is boilerplate:

// match version: boilerplate for the case we ignore
match opt { Some(v) => run(v), _ => () }
// if let: say what you mean
if let Some(v) = opt { run(v) }

The trade-off: if let gives up exhaustiveness checking — the compiler won't nag if you add a variant later. So if let for "if it looks like this, do X; otherwise carry on"; match when every case deserves thought.

What is let-else and why was it added to the language?

It's the inverted if let — for when the happy path should fall out of the pattern and the unhappy path should bail:

let Some(v) = opt else {
    return;          // else block must diverge (return/continue/panic)
};
use(v);              // v bound in the outer scope now

Unlike if let, the binding escapes to the surrounding scope — the guard clause pattern without nesting your real logic one level deeper. (Additional context: let-else arrived in Rust 1.65; the Brown edition covers it.)