Structs and Enums

Structs and enums are the primary ways to create custom data types in Rust. Together with pattern matching, they form the backbone of Rust's type system.

For Python developers: Structs are similar to dataclasses or simple classes. Enums are much more powerful than Python's Enum - they can hold 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 entire struct must be mutable - 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

In the next chapter, we'll explore Rust's built-in 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);
}