[Rust Series 9] Expressing State with Enums and Pattern Matching

한국어 버전

After grouping data with structs, the next step is defining the states that data can occupy. A Rust enum represents "exactly one of several forms," and [[match|match]] lets you branch on those forms. [[option|Option]] is a standard enum for "value present or absent." This part upgrades our study log with enums to make state updates clearer.

Enum Basics

enum StudyState {
    Planned,
    InProgress { percent: u8 },
    Done,
}

fn main() {
    let state = StudyState::InProgress { percent: 40 };

    match state {
        StudyState::Planned => println!("Not started"),
        StudyState::InProgress { percent } => println!("{}% in progress", percent),
        StudyState::Done => println!("Finished"),
    }
}

Each variant can carry its own data. [[match|match]] must account for all variants, and you can destructure fields right inside the arms.

Combining Structs and Enums

Let’s extend the earlier StudyLog by storing state as an enum.

enum Progress {
    Todo,
    Doing(u8),
    Done,
}

struct StudyLog {
    topic: String,
    state: Progress,
    notes: Vec<String>,
}

impl StudyLog {
    fn new(topic: &str) -> Self {
        Self {
            topic: topic.to_string(),
            state: Progress::Todo,
            notes: Vec::new(),
        }
    }

    fn update(&mut self, progress: Progress) {
        self.state = progress;
    }

    fn render(&self) -> String {
        let label = match &self.state {
            Progress::Todo => "Waiting",
            Progress::Doing(p) => {
                if *p >= 100 { "Almost done" } else { "In progress" }
            }
            Progress::Done => "Finished",
        };

        format!("{} ({})", self.topic, label)
    }
}

fn main() {
    let mut log = StudyLog::new("enum");
    log.update(Progress::Doing(60));
    println!("{}", log.render());

    log.update(Progress::Done);
    println!("{}", log.render());
}

Because match is an expression, you can build strings or numbers directly in an arm and return them. Enums constrain state to the handful of values you care about, so there’s less guessing than with ad‑hoc conditionals.

Representing Value Presence with Option

[[option|Option<T>]] is simply an enum with two variants: Some(T) or None. It replaces null and lets the type system enforce "maybe there’s data." You can imagine it as a library-provided enum, just like the ones you write yourself.

struct StudyLog {
    topic: String,
    last_note: Option<String>,
}

impl StudyLog {
    fn add_note(&mut self, note: &str) {
        self.last_note = Some(note.to_string());
    }

    fn last_note(&self) -> Option<&str> {
        self.last_note.as_deref()
    }
}

fn main() {
    let mut log = StudyLog {
        topic: String::from("Option"),
        last_note: None,
    };

    if let Some(text) = log.last_note() {
        println!("{}", text);
    } else {
        println!("No notes yet");
    }

    log.add_note("Some/None pattern");
    println!("Latest note: {}", log.last_note().unwrap());
}

as_deref converts Option<String> into Option<&str> for free. unwrap panics if the value is None, so keep it for quick demos and prefer match or if let elsewhere.

A quick cheat sheet:

  • [[match|match]]: handle all variants exhaustively.
  • if let: drill into one variant and ignore the rest.
  • unwrap: only in tests or tiny examples where absence would be a bug.

Pattern-Matching Tips

  • Because match must mention every variant, the compiler warns you when a new variant lacks handling.
  • The _ pattern groups leftover cases, but overusing it can hide missing logic when enums grow.
  • if let keeps code short when you only care about a single pattern. Example: if let Progress::Doing(percent) = log.state { ... }

Practice in CodeSandbox

The sandbox below uses CodeSandbox's Rust starter. Move the main code into src/main.rs, then compare cargo check and cargo run so you can read the compiler feedback beside the final output.

Live Practice

Rust Practice Sandbox

CodeSandbox

Run the starter project in CodeSandbox, compare it with the lesson code, and keep experimenting.

Rust startercargoterminal
  1. Fork the starter and open src/main.rs
  2. Paste the lesson code and run cargo check plus cargo run in order
  3. Change types, values, or borrowing flow and compare the compiler feedback with the output

Rust practice here is mainly terminal-driven rather than browser-preview driven. Lessons that need multiple files or extra crates may require a bit more setup inside the starter.

Try It Yourself

  • Add a Blocked(String) variant to Progress and print the reason inside render.
  • Sketch how you’d use [[result|Result]] instead of [[option|Option]] to carry error messages. (We’ll go deep in part 11.)
  • Practice matching multiple values at once: e.g., inspect Progress::Doing(p) and notes.len() at the same time to craft a richer message.

Wrap-Up

Enums plus pattern matching are Rust’s way to express state explicitly. Once you’re comfortable with common enums like Option, you can avoid null pointers entirely and let match ensure you cover every branch. Next we’ll shift to collections like Vec and HashMap and plug our structs and enums into real data sets.

💬 댓글

이 글에 대한 의견을 남겨주세요