Structs & Enums

Structs and enums are Rust's primary tools for creating custom data types. They allow you to group related data together and create meaningful types for your domain.

Defining Structs

A struct (short for "structure") is a custom data type that groups related values together:

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

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

    println!("Username: {}", user1.username);
}

Mutable Structs

To modify a struct, the entire instance must be mutable:

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

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

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

Struct Update Syntax

Create a new instance from an existing one:

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

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

    // Create user2 with some values from user1
    let user2 = User {
        email: String::from("another@example.com"),
        ..user1  // Take remaining fields from user1
    };

    println!("User2: {}", user2.username);
}

Tuple Structs

Structs that look like tuples, useful for giving meaning to tuples:

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

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

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

Methods on Structs

Use impl blocks to define methods:

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    // Method that borrows self
    fn area(&self) -> u32 {
        self.width * self.height
    }

    // Method with additional parameters
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }

    // Associated function (no self) - often used as constructors
    fn square(size: u32) -> Rectangle {
        Rectangle {
            width: size,
            height: size,
        }
    }
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    let rect2 = Rectangle { width: 10, height: 40 };
    let square = Rectangle::square(25);

    println!("Area of rect1: {}", rect1.area());
    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Square area: {}", square.area());
}

Defining Enums

Enums allow you to define a type by enumerating its possible variants:

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

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

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

Enums with Data

Enum variants can hold data of different types:

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

fn main() {
    let msg1 = Message::Quit;
    let msg2 = Message::Move { x: 10, y: 20 };
    let msg3 = Message::Write(String::from("Hello"));
    let msg4 = Message::ChangeColor(255, 0, 0);

    process_message(msg3);
}

fn process_message(msg: Message) {
    match msg {
        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!("Color: RGB({}, {}, {})", r, g, b),
    }
}

The Option Enum

Rust's Option type handles the absence of a value (no null!):

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

    // Using match
    match some_number {
        Some(n) => println!("Got number: {}", n),
        None => println!("No number"),
    }

    // Using if let for simpler cases
    if let Some(n) = some_number {
        println!("The number is: {}", n);
    }
}

Methods on Enums

Enums can have methods too:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

impl Coin {
    fn value_in_cents(&self) -> u32 {
        match self {
            Coin::Penny => 1,
            Coin::Nickel => 5,
            Coin::Dime => 10,
            Coin::Quarter => 25,
        }
    }
}

fn main() {
    let coin = Coin::Quarter;
    println!("Value: {} cents", coin.value_in_cents());
}

Practice Exercise

Try this in the playground:

struct Person {
    name: String,
    age: u32,
}

impl Person {
    fn new(name: &str, age: u32) -> Person {
        Person {
            name: String::from(name),
            age,
        }
    }

    fn greet(&self) {
        println!("Hi, I'm {} and I'm {} years old!", self.name, self.age);
    }

    fn have_birthday(&mut self) {
        self.age += 1;
        println!("Happy birthday! Now I'm {}!", self.age);
    }
}

fn main() {
    let mut person = Person::new("Alice", 30);
    person.greet();
    person.have_birthday();
}

Key Takeaways

  • Structs group related data with named fields
  • Use impl blocks to add methods to structs
  • Enums define types with multiple variants
  • Enum variants can hold different types of data
  • Option<T> replaces null with Some(T) or None
  • match exhaustively handles all enum variants
  • if let provides a simpler syntax for single-variant matching

Structs and enums are foundational to writing idiomatic Rust code!

Structs & Enums | LearningRust.org