← Back to Rust series
🦀
Type system
Type system · Prerequisite: lesson 11

12. Enums + Introducing Option<T>

Rust enums are a different beast from C enums — each variant can carry **different data of different types**. They're true sum types. The most-used enum, `Option<T>` (`Some(T)` or `None`), is how Rust avoids the billion-dollar null-pointer mistake.

RustenumOptionsum typenull safety
Duration
~1.5 hours
Level
📊 Intermediate
Prerequisite
🎯 Lesson 11
OUTCOME
Rust enums are a different beast from C enums — each variant can carry **different data of different types**. They're true sum types. The most-used enum, `Option<T>` (`Some(T)` or `None`), is how Rust avoids the billion-dollar null-pointer mistake.

What you'll learn

  • 1Attach different payload types to enum variants
  • 2Express optional values with `Option<T>` instead of null
  • 3Destructure enums with `match` and `if let`
  • 4Use `.unwrap()` / `.unwrap_or()` / `.map()` on Option
  • 5Explain how a null-free language is safer

Overview

In 2009 Tony Hoare called the 1965 invention of null pointers his "billion-dollar mistake." Rust removes null from the language entirely and replaces it with `Option<T>` — the **type system makes absence explicit**, so the compiler catches forgotten checks.

Core Concepts

1) Rust enums are sum types

rust
enum Message {
    Quit,                          // no data
    Move { x: i32, y: i32 },       // named struct payload
    Write(String),                  // tuple of 1
    ChangeColor(i32, i32, i32),     // tuple of 3
}

Each variant is essentially "one case" of a different shape. Not just integer aliases like C enums.

2) Option<T> is built in

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

It's in the prelude, so you write `Option<T>` and `Some(...)` / `None` without imports.

3) Destructuring — match / if let

  • **match** — every variant must be handled (compile error if you miss one)
  • **if let** — short form when you only care about one variant

4) Option helpers

MethodMeaning
.unwrap()Some → inner, None → panic
.unwrap_or(d)Some → inner, None → d
.map(|x| ...)Transform Some only
.and_then(|x| ...)Transform Some, return Option
.is_some() / .is_none()Boolean checks

Hands-on Examples

match with full variant coverage:

rust
enum Coin { Penny, Nickel, Dime, Quarter }

fn value(c: Coin) -> u32 {
    match c {
        Coin::Penny   => 1,
        Coin::Nickel  => 5,
        Coin::Dime    => 10,
        Coin::Quarter => 25,
    }
}

fn main() { println!("{}", value(Coin::Quarter)); }

Working with Option<T>:

rust
fn find_first_even(v: &[i32]) -> Option<i32> {
    for &x in v {
        if x % 2 == 0 { return Some(x); }
    }
    None
}

fn main() {
    let v = [1, 3, 4, 7, 8];
    match find_first_even(&v) {
        Some(x) => println!("found {}", x),
        None    => println!("not found"),
    }
    if let Some(x) = find_first_even(&v) { println!("got {}", x); }
    let v2: [i32; 0] = [];
    let result = find_first_even(&v2).unwrap_or(-1);
    println!("{}", result); // -1
}

Data-carrying variants:

rust
enum Shape {
    Circle(f64),
    Rectangle { w: f64, h: f64 },
}

fn area(s: Shape) -> f64 {
    match s {
        Shape::Circle(r)            => std::f64::consts::PI * r * r,
        Shape::Rectangle { w, h }   => w * h,
    }
}

fn main() {
    println!("{}", area(Shape::Circle(2.0)));
    println!("{}", area(Shape::Rectangle { w: 3.0, h: 4.0 }));
}

Common Mistakes

Q. Can I just use .unwrap() everywhere?

A. Fine for prototypes / tests. Real code should prefer `.unwrap_or(...)` / `.map` / the `?` operator (coming up) to avoid runtime panics in failure paths.

Q. Match is verbose. Can I use a wildcard?

A. `_ =>` matches everything else. But explicit variants are safer — when you add a new variant later, the compiler will tell you which match arms need updating.

Q. Is the no-null thing really that big a deal?

A. In Java/Python the NullPointerException happens at runtime. Rust makes `None` impossible to silently pass in place of `T` — handling absence becomes a compile-time requirement.

Recap

  • Each enum variant can hold different data (sum type)
  • `Option<T>` replaces null — absence is encoded in the type
  • Destructure with match (exhaustive) or if let (single case)
  • Useful helpers: .unwrap / .unwrap_or / .map

Try It Yourself

  1. Write `fn find(v: &[i32], target: i32) -> Option<usize>`
  2. Define `enum Event { Login(String), Logout, Click { x: i32, y: i32 } }` and match each variant to a different message
  3. Write a function adding two Option<i32>: both Some → Some(sum), any None → None
Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub ↗