Chapter 4: Structs & Enums
Real-world data is rarely isolated. Rust uses structs to bundle related fields into a custom type and enums to express "a value may be one of several forms." Paired with pattern matching, these let you model a domain precisely — and make illegal states unrepresentable at compile time.
Learning Objectives
- Define structs, methods, and associated functions.
- Model "one-of" data with enums, and understand
OptionandResultas enums. - Use
matchandif letfor pattern matching. - Attach behavior to types with
implblocks.
4.1 Structs
A struct groups named fields into a type. Three forms: named-field, tuple, and unit:
// Named-field — most common struct User { name: String, age: u32, active: bool, } // Tuple struct — fields unnamed, lightweight wrapper struct Color(i32, i32, i32); // Unit struct — no fields, often used with traits struct Marker; fn main() { let u = User { name: "alice".into(), age: 30, active: true }; println!("{} {} {}", u.name, u.age, u.active); let c = Color(255, 128, 0); println!("{} {} {}", c.0, c.1, c.2); }
Field privacy: struct fields are private by default; accessing them outside their module requires
pub(Chapter 8).
Field shorthand & update syntax
When a variable name matches a field name you can shorthand; .. copies the rest:
fn main() { let name = String::from("bob"); let u1 = User { name, age: 25, active: true }; // name shorthand let u2 = User { age: 26, ..u1 }; // rest copied from u1 println!("{} {}", u2.name, u2.age); }
Note:
..u1moves fields out ofu1. Sinceu1.nameis moved,u1as a whole is no longer usable (unless all copied fields areCopy).
4.2 Methods & Associated Functions: impl
Use impl to attach behavior. An fn with &self/&mut self/self is a method; without self it is an associated function (like a static method):
struct Rectangle { width: f64, height: f64, } impl Rectangle { // Associated function — a constructor, like Rectangle::new fn new(width: f64, height: f64) -> Self { Rectangle { width, height } } // Method — borrows self fn area(&self) -> f64 { self.width * self.height } // Method — mutable borrow fn scale(&mut self, factor: f64) { self.width *= factor; self.height *= factor; } } fn main() { let mut r = Rectangle::new(3.0, 4.0); println!("area = {}", r.area()); // 12 r.scale(2.0); println!("area = {}", r.area()); // 48 }
Self is an alias for the current type. You may write multiple impl blocks, often to group methods by concern.
4.3 Enums: One-of Values
An enum represents a value that may be one of several variants. Rust's enums are powerful — each variant can carry different types and amounts of data:
enum Message { Quit, // no data Move { x: i32, y: i32 }, // named fields Write(String), // one value ChangeColor(i32, i32, i32), // tuple } fn main() { let m = Message::Write("hello".into()); process(m); } fn process(msg: Message) { // Must handle every variant — the compiler enforces exhaustiveness match msg { Message::Quit => println!("quit"), Message::Move { x, y } => println!("move to {x},{y}"), Message::Write(text) => println!("write: {text}"), Message::ChangeColor(r, g, b) => println!("color {r},{g},{b}"), } }
Enum vs struct: use an enum when a value "is A or B or C"; use a struct when it "has A and B and C."
Option<T>: a standard-library enum
Option uses an enum to express "a value or nothing," replacing null:
#![allow(unused)] fn main() { enum Option<T> { Some(T), None, } }
There is no null in Rust — to represent possible absence, use Option<T>, and the compiler forces you to handle None. Chapter 6 expands on its role in error handling.
4.4 Pattern Matching: match
match does exhaustive branching on an enum — one of Rust's most powerful control-flow constructs:
fn describe(n: i32) -> &'static str { match n { 0 => "zero", 1..=9 => "single digit", 10 | 20 | 30 => "round ten", _ if n < 0 => "negative", // guard _ => "other", } } fn main() { println!("{}", describe(0)); println!("{}", describe(7)); println!("{}", describe(-3)); }
Key points:
- Must be exhaustive;
_is the catch-all. - Arms can bind variables (e.g.
Message::Move { x, y }bindsx/yto the field values). - You can add a guard (
if condition) for extra filtering.
if let: caring about one arm
When you want to handle only one case and ignore the rest, if let is more concise than match:
fn main() { let m = Message::Write("hi".into()); if let Message::Write(text) = m { println!("writing: {text}"); } else { println!("not Write"); } }
while let works similarly, for repeated deconstruction in a loop.
4.5 Worked Example: A State Machine
Model an order state machine with an enum and pattern matching — an illegal transition cannot even be written:
enum OrderState { Pending, Paid, Shipped, Delivered, Cancelled, } impl OrderState { fn next(self) -> OrderState { match self { OrderState::Pending => OrderState::Paid, OrderState::Paid => OrderState::Shipped, OrderState::Shipped => OrderState::Delivered, // Delivered or Cancelled — no next state OrderState::Delivered | OrderState::Cancelled => self, } } fn label(&self) -> &'static str { match self { OrderState::Pending => "pending", OrderState::Paid => "paid", OrderState::Shipped => "shipped", OrderState::Delivered => "delivered", OrderState::Cancelled => "cancelled", } } } fn main() { let mut s = OrderState::Pending; for _ in 0..4 { println!("{}", s.label()); s = s.next(); } }
This shows the core value of enums: encode business rules into the type system, so "delivered then back to pending" cannot be expressed at all.
4.6 Summary
Structs bundle related fields into custom types; enums express "one-of" values; impl blocks attach behavior; match does exhaustive branching. Option replaces null and forces you to handle absence. Used together, they encode business constraints into types — making illegal states unrepresentable, which is the heart of Rust's type-safe design.
Exercises
- Define a
Pointstruct and aShapeenum (Circle,Rectangle,Triangle); usematchto compute each shape's area. - Add a
birthday(&mut self)method toUserthat incrementsage, and an associated functionUser::new(name, age). - Write a function returning the first positive number from a list as
Option<i32>, and handle the result withif let.