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
matchmust 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 letkeeps 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.
💬 댓글
이 글에 대한 의견을 남겨주세요