Day 3 · Concept 07
Structs & enums
struct = product type. A bundle of named fields, all present.
enum = sum type. One of several variants — each variant can carry
different data. Together they cover every shape your program needs.
1 · Structs
struct User {
name: String,
age: u32,
active: bool,
}
impl User {
// Associated function (constructor)
fn new(name: String, age: u32) -> Self {
Self { name, age, active: true }
}
// Method — takes &self (immutable reference to itself)
fn greet(&self) -> String {
format!("Hi, I am {}", self.name)
}
}
fn main() {
let alice = User::new("Alice".into(), 30);
println!("{}", alice.greet());
}2 · Three flavours of struct
// Named-field struct — the most common
struct Point { x: f64, y: f64 }
// Tuple struct — fields by position
struct Color(u8, u8, u8);
// Unit struct — no fields. Useful for type-level markers.
struct Eof;3 · Enums — the killer feature
enum Shape {
Circle(f64), // radius
Rectangle { w: f64, h: f64 }, // named fields per variant
Point, // no data
}
fn area(s: &Shape) -> f64 {
match s {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle { w, h } => w * h,
Shape::Point => 0.0,
}
}
fn main() {
let shapes = vec![
Shape::Circle(2.0),
Shape::Rectangle { w: 3.0, h: 4.0 },
Shape::Point,
];
for s in &shapes {
println!("area = {}", area(s));
}
}This is what other languages call "tagged unions". An enum
variant can carry different data per variant. Pattern matching destructures
cleanly. Add a new variant; the compiler tells you every match site that needs
updating. Refactoring with the type system on your side.
4 · The two built-in enums that change everything
// In the standard library:
enum Option<T> { Some(T), None }
enum Result<T, E> { Ok(T), Err(E) }
// No null. No exceptions. Every fallible value is one of these.These two enums are how Rust replaces null pointers and exceptions. The compiler forces you to handle both variants — no forgetting.
5 · Derive traits — auto-implement
#[derive(Debug, Clone, PartialEq)]
struct Point { x: f64, y: f64 }
fn main() {
let p = Point { x: 1.0, y: 2.0 };
println!("{:?}", p); // Debug — for printing
let q = p.clone(); // Clone — explicit copy
println!("equal? {}", p == q); // PartialEq — for ==
}The common derives.
Debug for {:?}
printing. Clone for .clone(). Copy for
trivial types. PartialEq for ==. Eq +
Hash for map keys. Default for ::default().6 · Common mistakes
- Forgetting
#[derive(Debug)]. Thenprintln!("{:?}", x)won't compile. - Using enum for things that should be enum. Trying to encode "string maybe int" via two related structs — make it one enum.
- Forgetting to handle every variant in
match. Won't compile — exhaustiveness. Use_ =>for catch-all. - Copy on heap types. Can't derive Copy on a struct containing String or Vec. Use Clone and call .clone() explicitly.
7 · When it clicks
- You reach for enum to model "one of these states" — never a bool + comment.
- The compiler-driven refactor (add a variant, fix every site) becomes your favourite tool.
- You derive Debug+Clone+PartialEq on new types reflexively.
Found this useful?