Rust / Basic / Lesson 5
Structs, enums, and pattern matching
Model related data and finite states so meaningful operations preserve invariants and match expressions handle every required alternative.
On this page
Lesson goals
- Define named fields, methods, and construction boundaries.
- Represent mutually exclusive states with enum payloads.
- Match borrowed data without unintentionally moving it.
Name the valid states first
A struct groups fields that belong to one value, and an impl block defines associated functions and methods. An invariant describes the valid relationship between its fields. A stock quantity may never be negative; a rectangle might require positive dimensions. Choose public operations around those rules instead of exposing every field for unrestricted editing. Rust memory safety does not automatically enforce business invariants. Private fields and validated construction help limit which code can create or modify the representation, making the obligation smaller and easier to test.
A method states its access requirements
A method taking &self observes through a shared borrow; &mut self requests exclusive mutable access, and self consumes the value. Associated constructors often use a new name, but new has no special language meaning. The stock example checks the full request before subtraction. Unsigned subtraction would be inappropriate if an excessive request were allowed through. Returning bool communicates acceptance without inventing an invalid intermediate state. Tests should inspect the remaining quantity as well as the return value, because rejecting a request must leave the stock unchanged.
struct Stock { quantity: u32 }
impl Stock {
fn withdraw(&mut self, amount: u32) -> bool {
if amount > self.quantity { return false; }
self.quantity -= amount;
true
}
}
fn main() {
let mut stock = Stock { quantity: 10 };
let first = stock.withdraw(3);
let second = stock.withdraw(8);
println!("{first} {second} {}", stock.quantity);
}
true false 7
Three units leave seven. Eight is rejected, so the quantity remains seven and no unsigned underflow occurs.
Use enum variants for alternatives
An enum represents one active variant at a time, and variants can carry different payload types. This avoids parallel flags that permit contradictory combinations such as ready and failed simultaneously. Option<T> is a standard enum for a present or absent value; it makes absence explicit rather than assigning a magic integer. A domain enum can attach units only to Ready and an explanatory String only to Failed. Construction then records which data belongs with which state, although payload validation still remains an application responsibility.
Handle the shape of every state
match selects an arm by pattern and is exhaustive for the matched type. The second example borrows &Job, so matching does not consume its owned error message. Pattern binding gives access to the active payload, and each arm produces a String. A wildcard arm can be useful, but it also hides future variants that might deserve a new policy. Use explicit alternatives when exhaustive review is valuable. if let is convenient when only one interesting pattern matters, while match better communicates a complete state-dependent operation.
enum Job { Ready(u32), Paused, Failed(String) }
fn describe(job: &Job) -> String {
match job {
Job::Ready(units) => format!("ready: {units}"),
Job::Paused => String::from("paused"),
Job::Failed(reason) => format!("failed: {reason}"),
}
}
fn main() {
for job in [Job::Ready(3), Job::Paused, Job::Failed(String::from("disk"))] {
println!("{}", describe(&job));
}
}
ready: 3
paused
failed: disk
Each state chooses exactly one arm. The descriptions own their returned text, and the input enum is only borrowed.
Patterns also participate in ownership
Destructuring an owned value can move non-Copy fields, while matching a reference usually creates borrowed bindings through match ergonomics. Understand the scrutinee type before adding clone or dereference operators. Guards add conditions after a pattern matches and do not replace handling the remaining possibilities. Keep public constructors responsible for validating payload ranges. Later modules can hide representation and expose transition methods, allowing implementation changes without rewriting callers. A strong model reduces impossible combinations, but it still needs tests for legal transitions, rejected operations, and formatting or serialization policies.
Common errors and fixes
A match omits a variant.
- Why it happens
- The operation does not cover the full enum domain.
- How to fix it
- Add the required arm with an explicit policy.
- Prevent it next time
- Prefer exhaustive alternatives when states evolve.
Matching consumes an error message unexpectedly.
- Why it happens
- An owned enum was destructured by value.
- How to fix it
- Match a reference when observation is sufficient.
- Prevent it next time
- Review scrutinee ownership before binding payloads.
A stock operation changes state before rejection.
- Why it happens
- Validation happened after arithmetic.
- How to fix it
- Validate the transition before changing fields.
- Prevent it next time
- Test return values together with resulting state.
Check your understanding
1. Apply withdrawals zero, ten, and one to a fresh stock of ten, in that order. Give each result and the final quantity.
Show worked solution
Zero succeeds and leaves ten, ten succeeds and leaves zero, then one fails and leaves zero. The results are true, true, false. No request subtracts before the bounds check, so the unsigned representation never needs to describe a negative stock. Add these as separate state-transition cases rather than only comparing a final line of text.
2. Add a Cancelled variant with a reason String to Job. Explain the changes required by describe and why a wildcard might weaken review.
Show worked solution
Add Job::Cancelled(String) and an explicit borrowed match arm that returns a formatted cancellation description. The compiler identifies the missing arm in an exhaustive match. A preexisting wildcard could silently absorb cancellation without showing its reason, so explicit state handling is useful when every new state needs a deliberate user-facing policy.
Key takeaways
- Model alternatives with enum payloads.
- Validate transitions before mutation.
- Patterns and methods express ownership as well as behavior.