Structs and Enums

Custom data types in two shapes: structs group related fields, enums describe values that can be one of several variants. Together with pattern matching, they cover most of the type-design work you do in Rust.

For Python developers: structs are like dataclasses. Enums go further than Python's Enum. Each variant can hold its own data.

Structs

Defining Structs

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

fn main() {
    let user = User {
        email: String::from("user@example.com"),
        username: String::from("someuser"),
        active: true,
        sign_in_count: 1,
    };

    println!("User: {}", user.username);
}

Python comparison:

# Using dataclass (Python 3.7+)
from dataclasses import dataclass

@dataclass
class User:
    username: str
    email: str
    sign_in_count: int
    active: bool

user = User(
    email="user@example.com",
    username="someuser",
    active=True,
    sign_in_count=1
)

# Or traditional class
class User:
    def __init__(self, username, email, sign_in_count, active):
        self.username = username
        self.email = email
        self.sign_in_count = sign_in_count
        self.active = active

Mutable Structs

The whole struct is either mutable or not. You can't mark individual fields.

fn main() {
    let mut user = User {
        email: String::from("user@example.com"),
        username: String::from("someuser"),
        active: true,
        sign_in_count: 1,
    };

    user.email = String::from("new@example.com");
    println!("Email: {}", user.email);
}

Field Init Shorthand

When parameter names match field names:

fn build_user(email: String, username: String) -> User {
    User {
        email,       // Shorthand for email: email
        username,    // Shorthand for username: username
        active: true,
        sign_in_count: 1,
    }
}

Struct Update Syntax

Create a new struct from an existing one:

fn main() {
    let user1 = User {
        email: String::from("user1@example.com"),
        username: String::from("user1"),
        active: true,
        sign_in_count: 1,
    };

    let user2 = User {
        email: String::from("user2@example.com"),
        ..user1  // Copy remaining fields from user1
    };

    // user1.email still valid (String was not moved)
    // user1.username was moved to user2
    println!("User2: {}", user2.email);
}

Warning: If the update syntax moves a non-Copy field, the original is partially moved.

Tuple Structs

Named tuples with no field names:

struct Color(i32, i32, i32);
struct Point(f64, f64, f64);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0.0, 0.0, 0.0);

    println!("Red: {}", black.0);
    println!("X: {}", origin.0);
}

Useful when you want type safety but naming fields isn't needed.

Unit-Like Structs

Structs with no fields:

struct AlwaysEqual;

fn main() {
    let _subject = AlwaysEqual;
}

Useful for implementing traits on a type with no data.

Deriving Traits for Structs

#[derive(Debug, Clone, PartialEq)]
struct Rectangle {
    width: f64,
    height: f64,
}

fn main() {
    let rect = Rectangle { width: 30.0, height: 50.0 };

    // Debug print
    println!("{:?}", rect);
    println!("{:#?}", rect);  // Pretty print

    // Clone
    let rect2 = rect.clone();

    // Compare
    println!("Equal: {}", rect == rect2);
}
DerivePurpose
DebugEnable {:?} formatting
CloneEnable .clone()
CopyEnable implicit copies (requires Clone)
PartialEqEnable == and !=
EqFull equality (requires PartialEq)
HashEnable use as HashMap key
DefaultEnable Default::default()

Implementing Methods

#[derive(Debug)]
struct Rectangle {
    width: f64,
    height: f64,
}

impl Rectangle {
    // Associated function (constructor)
    fn new(width: f64, height: f64) -> Self {
        Rectangle { width, height }
    }

    fn square(size: f64) -> Self {
        Rectangle { width: size, height: size }
    }

    // Methods
    fn area(&self) -> f64 {
        self.width * self.height
    }

    fn perimeter(&self) -> f64 {
        2.0 * (self.width + self.height)
    }

    fn is_square(&self) -> bool {
        (self.width - self.height).abs() < f64::EPSILON
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }

    fn scale(&mut self, factor: f64) {
        self.width *= factor;
        self.height *= factor;
    }
}

fn main() {
    let mut rect = Rectangle::new(30.0, 50.0);
    println!("Area: {}", rect.area());
    println!("Perimeter: {}", rect.perimeter());
    println!("Is square: {}", rect.is_square());

    rect.scale(2.0);
    println!("Scaled: {:?}", rect);

    let sq = Rectangle::square(10.0);
    println!("Can hold square: {}", rect.can_hold(&sq));
}

You can have multiple impl blocks for the same struct.

Enums

Defining Enums

enum Direction {
    North,
    South,
    East,
    West,
}

fn main() {
    let dir = Direction::North;

    match dir {
        Direction::North => println!("Going north"),
        Direction::South => println!("Going south"),
        Direction::East => println!("Going east"),
        Direction::West => println!("Going west"),
    }
}

Enums with Data

Each variant can hold different types and amounts of data:

enum Message {
    Quit,                       // No data
    Move { x: i32, y: i32 },   // Named fields (like a struct)
    Write(String),              // Single String
    ChangeColor(i32, i32, i32), // Three i32s
}

fn main() {
    let msg = Message::Write(String::from("hello"));

    match msg {
        Message::Quit => println!("Quit"),
        Message::Move { x, y } => println!("Move to ({}, {})", x, y),
        Message::Write(text) => println!("Text: {}", text),
        Message::ChangeColor(r, g, b) => println!("Color: ({}, {}, {})", r, g, b),
    }
}

Methods on Enums

impl Message {
    fn call(&self) {
        match self {
            Message::Quit => println!("Quitting"),
            Message::Move { x, y } => println!("Moving to ({}, {})", x, y),
            Message::Write(text) => println!("Writing: {}", text),
            Message::ChangeColor(r, g, b) => {
                println!("Changing color to ({}, {}, {})", r, g, b);
            }
        }
    }
}

fn main() {
    let msg = Message::Move { x: 10, y: 20 };
    msg.call();
}

The Option Enum

Rust has no null. Instead it uses Option<T>:

enum Option<T> {
    Some(T),
    None,
}
fn main() {
    let some_number: Option<i32> = Some(5);
    let no_number: Option<i32> = None;

    // You must handle both cases
    match some_number {
        Some(n) => println!("Got: {}", n),
        None => println!("Got nothing"),
    }
}

Option Methods

fn main() {
    let x: Option<i32> = Some(42);
    let y: Option<i32> = None;

    // unwrap - panics if None
    println!("{}", x.unwrap());

    // unwrap_or - default value
    println!("{}", y.unwrap_or(0));

    // unwrap_or_else - lazy default
    println!("{}", y.unwrap_or_else(|| 2 * 21));

    // map - transform inner value
    let doubled = x.map(|n| n * 2);
    println!("{:?}", doubled);  // Some(84)

    // and_then - chain operations
    let result = x
        .map(|n| n + 1)
        .and_then(|n| if n > 40 { Some(n) } else { None });
    println!("{:?}", result);  // Some(43)

    // is_some / is_none
    println!("x is some: {}", x.is_some());
    println!("y is none: {}", y.is_none());
}
MethodBehavior on NoneBehavior on Some(v)
unwrap()PanicsReturns v
unwrap_or(default)Returns defaultReturns v
unwrap_or_else(f)Returns f()Returns v
map(f)Returns NoneReturns Some(f(v))
and_then(f)Returns NoneReturns f(v)
is_some()falsetrue

Pattern Matching in Depth

Destructuring Structs

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    // Destructure in let binding
    let Point { x, y } = p;
    println!("x: {}, y: {}", x, y);

    // Destructure in match
    match p {
        Point { x: 0, y } => println!("On y-axis at {}", y),
        Point { x, y: 0 } => println!("On x-axis at {}", x),
        Point { x, y } => println!("At ({}, {})", x, y),
    }
}

Destructuring Enums

enum Shape {
    Circle(f64),
    Rectangle(f64, f64),
    Triangle { base: f64, height: f64 },
}

fn area(shape: &Shape) -> f64 {
    match shape {
        Shape::Circle(radius) => std::f64::consts::PI * radius * radius,
        Shape::Rectangle(width, height) => width * height,
        Shape::Triangle { base, height } => 0.5 * base * height,
    }
}

fn main() {
    let shapes = vec![
        Shape::Circle(5.0),
        Shape::Rectangle(4.0, 6.0),
        Shape::Triangle { base: 3.0, height: 8.0 },
    ];

    for shape in &shapes {
        println!("Area: {:.2}", area(shape));
    }
}

Nested Patterns

enum Color {
    Rgb(i32, i32, i32),
    Hsv(i32, i32, i32),
}

enum Message {
    ChangeColor(Color),
    Move { x: i32, y: i32 },
}

fn main() {
    let msg = Message::ChangeColor(Color::Rgb(0, 160, 255));

    match msg {
        Message::ChangeColor(Color::Rgb(r, g, b)) => {
            println!("RGB: ({}, {}, {})", r, g, b);
        }
        Message::ChangeColor(Color::Hsv(h, s, v)) => {
            println!("HSV: ({}, {}, {})", h, s, v);
        }
        Message::Move { x, y } => {
            println!("Move to ({}, {})", x, y);
        }
    }
}

Ignoring Values

fn main() {
    let numbers = (1, 2, 3, 4, 5);

    // Ignore specific fields with _
    match numbers {
        (first, _, third, _, fifth) => {
            println!("{} {} {}", first, third, fifth);
        }
    }

    // Ignore remaining with ..
    match numbers {
        (first, .., last) => {
            println!("first: {}, last: {}", first, last);
        }
    }

    // Ignore entire value with _
    let _unused = 42;
}

Binding with @

Bind a value while also testing it:

fn main() {
    let num = 5;

    match num {
        n @ 1..=5 => println!("Got {} (between 1 and 5)", n),
        n @ 6..=10 => println!("Got {} (between 6 and 10)", n),
        n => println!("Got {} (something else)", n),
    }
}

Combining Structs and Enums

State Machine

#[derive(Debug)]
enum OrderStatus {
    Pending,
    Processing,
    Shipped { tracking: String },
    Delivered,
    Cancelled { reason: String },
}

struct Order {
    id: u32,
    item: String,
    quantity: u32,
    status: OrderStatus,
}

impl Order {
    fn new(id: u32, item: &str, quantity: u32) -> Self {
        Order {
            id,
            item: item.to_string(),
            quantity,
            status: OrderStatus::Pending,
        }
    }

    fn ship(&mut self, tracking: String) {
        self.status = OrderStatus::Shipped { tracking };
    }

    fn cancel(&mut self, reason: &str) {
        self.status = OrderStatus::Cancelled {
            reason: reason.to_string(),
        };
    }

    fn summary(&self) -> String {
        let status_msg = match &self.status {
            OrderStatus::Pending => "awaiting processing".to_string(),
            OrderStatus::Processing => "being processed".to_string(),
            OrderStatus::Shipped { tracking } => format!("shipped ({})", tracking),
            OrderStatus::Delivered => "delivered".to_string(),
            OrderStatus::Cancelled { reason } => format!("cancelled: {}", reason),
        };

        format!(
            "Order #{}: {}x {} - {}",
            self.id, self.quantity, self.item, status_msg
        )
    }
}

fn main() {
    let mut order = Order::new(1, "Rust Book", 2);
    println!("{}", order.summary());

    order.ship("TRACK123".to_string());
    println!("{}", order.summary());
}

Enum as Configuration

enum LogLevel {
    Error,
    Warning,
    Info,
    Debug,
}

struct Logger {
    level: LogLevel,
}

impl Logger {
    fn log(&self, level: &LogLevel, message: &str) {
        let level_num = |l: &LogLevel| match l {
            LogLevel::Error => 0,
            LogLevel::Warning => 1,
            LogLevel::Info => 2,
            LogLevel::Debug => 3,
        };

        if level_num(level) <= level_num(&self.level) {
            let prefix = match level {
                LogLevel::Error => "ERROR",
                LogLevel::Warning => "WARN ",
                LogLevel::Info => "INFO ",
                LogLevel::Debug => "DEBUG",
            };
            println!("[{}] {}", prefix, message);
        }
    }
}

fn main() {
    let logger = Logger { level: LogLevel::Info };
    logger.log(&LogLevel::Error, "Something broke");
    logger.log(&LogLevel::Info, "Starting up");
    logger.log(&LogLevel::Debug, "This won't print");
}

Practice Exercises

Exercise 1: Student Records

Create a Student struct with name, grades (Vec<f64>), and a method to calculate the average.

struct Student {
    name: String,
    grades: Vec<f64>,
}

impl Student {
    fn average(&self) -> f64 {
        // Your code
    }
}

Exercise 2: Expression Evaluator

Create an enum for mathematical expressions and evaluate them:

enum Expr {
    Number(f64),
    Add(Box<Expr>, Box<Expr>),
    Multiply(Box<Expr>, Box<Expr>),
}

fn eval(expr: &Expr) -> f64 {
    // Your code
}

Exercise 3: Traffic Light

Create an enum for traffic light states and a method that returns the duration for each state.

Common Mistakes

1. Forgetting to Handle All Enum Variants

match direction {
    Direction::North => println!("north"),
    Direction::South => println!("south"),
}  // ERROR: non-exhaustive

Fix: Handle all variants or use _ catch-all.

2. Moving Fields in Update Syntax

let user2 = User { email: String::from("new@example.com"), ..user1 };
println!("{}", user1.username);  // ERROR: username was moved

Fix: Clone the source or only access non-moved fields.

3. Comparing Enums Without PartialEq

let d = Direction::North;
if d == Direction::North { }  // ERROR without #[derive(PartialEq)]

Fix: Add #[derive(PartialEq)] to the enum.

Key Takeaways

  • Structs group related data with named fields
  • Tuple structs are unnamed-field structs for simple wrappers
  • Enums represent types that can be one of several variants
  • Each enum variant can hold different types and amounts of data
  • Option<T> replaces null with compile-time safety
  • Pattern matching with match must be exhaustive
  • Use @ bindings to capture values while testing patterns
  • Structs and enums together model state machines and domain logic

Next Steps

Continue to 07-collections.md to learn the standard collections: Vec, String, and HashMap.

Quick Reference

// Struct
struct Point { x: f64, y: f64 }
let p = Point { x: 1.0, y: 2.0 };

// Tuple struct
struct Color(u8, u8, u8);
let red = Color(255, 0, 0);

// Enum
enum Shape {
    Circle(f64),
    Rect { w: f64, h: f64 },
}

// Pattern matching
match shape {
    Shape::Circle(r) => r * r * PI,
    Shape::Rect { w, h } => w * h,
}

// Option
let x: Option<i32> = Some(5);
let y: Option<i32> = None;

// if let
if let Some(val) = x {
    println!("{}", val);
}