07 / 20 · Day 3
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

rust src/main.rs
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

rust src/main.rs · the three shapes
// 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

rust src/main.rs · enums with data
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

rust src/main.rs · Option and Result
// 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

rust src/main.rs · #[derive]
#[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)]. Then println!("{:?}", 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?