PHASE 1 ← Back to Course
4 / 17
🏗️

Structs and Enums

Define custom types with structs, model variants with enums, and unlock Rust's powerful pattern matching.

1

Defining and Using Structs

A struct is a custom data type that groups together multiple related values. There are three types of structs in Rust.

Struct Definition

Rust
struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
fn main() {
    let mut user = User {
        username: String::from("john_doe"),
        email: String::from("[email protected]"),
        sign_in_count: 1,
        active: true,
    };
    user.sign_in_count += 1;
    println!("User: {} ({})", user.username, user.email);
    println!("Sign in count: {}", user.sign_in_count);
}

Field Init Shorthand

When variable names match field names, you can use shorthand syntax:

Rust
fn create_user(username: String, email: String) -> User {
    User {
        username,     // Same as username: username
        email,        // Same as email: email
        sign_in_count: 1,
        active: true,
    }
fn main() {
    let user = create_user(
        String::from("jane_doe"),
        String::from("[email protected]"),
    );
}

Struct Update Syntax

Create a new struct from an existing one, changing only some fields:

Rust
fn main() {
    let user1 = User {
        username: String::from("john"),
        email: String::from("[email protected]"),
        sign_in_count: 1,
        active: true,
    };
    let user2 = User {
        username: String::from("jane"),
        ..user1  // Use remaining fields from user1
    };
}

Tuple Structs

Structs without named fields:

Rust
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!("Color red: {}", black.0);
    println!("Point x: {}", origin.0);
}

Unit-like Structs

Structs without any fields:

Rust
struct AlwaysEqual;
fn main() {
    let subject = AlwaysEqual;
}
💡

Tip

Use the struct update syntax (..other_struct) to create new instances that share most fields with an existing struct. This keeps code DRY and readable.

2

Method Syntax

Methods are functions defined within the context of a struct (or enum, or trait). They take self as their first parameter.

Defining Methods with impl

Rust
struct Rectangle {
    width: u32,
    height: u32,
impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
    fn set_width(&mut self, width: u32) {
        self.width = width;
    }
fn main() {
    let mut rect1 = Rectangle { width: 30, height: 50 };
    let rect2 = Rectangle { width: 20, height: 40 };
    println!("Area: {}", rect1.area());
    println!("Can hold: {}", rect1.can_hold(&rect2));
    rect1.set_width(35);
}

Associated Functions

Associated functions don't take self and are called using the :: syntax:

Rust
impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle { width: size, height: size }
    }
fn main() {
    let sq = Rectangle::square(10);
    println!("Square area: {}", sq.area());
}

Key Insight

Associated functions (like Rectangle::square) act as constructors. They are called with :: instead of . because they don't operate on an instance.

3

Enums

Enums allow you to define a type with a set of possible variants. Each variant can have different data associated with it.

Basic Enum

Rust
enum Direction {
    North,
    South,
    East,
    West,
fn main() {
    let direction = Direction::North;
}

Enums with Associated Data

Rust
enum IpAddr {
    V4(String),
    V6(String),
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
fn main() {
    let ip1 = IpAddr::V4(String::from("127.0.0.1"));
    let ip2 = IpAddr::V6(String::from("::1"));
    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);
}

Methods on Enums

Rust
impl Message {
    fn call(&self) {
        println!("Calling message method");
    }
fn main() {
    let msg = Message::Write(String::from("hello"));
    msg.call();
}
💡

Tip

Rust enums are far more powerful than enums in most languages. Each variant can hold different types and amounts of data, making them ideal for modelling state machines and message passing.

4

The Option Enum

The Option enum is one of Rust's most powerful features. It represents an optional value:

Rust
enum Option<T> {
    Some(T),
    None,
}

Rust uses Option to represent values that might not exist. This eliminates the need for null pointers:

Rust
fn main() {
    let some_number = Some(5);
    let some_char = Some('e');
    let absent_number: Option<i32> = None;
    // Extract the value with match
    match some_number {
        Some(n) => println!("Number: {}", n),
        None => println!("No number"),
    }
}

Key Insight

Rust has no null. Instead, Option<T> forces you to explicitly handle the case where a value might be absent. The compiler won't let you use an Option<T> as if it were a T, you must unwrap it first.

5

Pattern Matching with match

The match expression lets you compare a value against multiple patterns and execute code based on which pattern matches:

Basic match

Rust
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
fn main() {
    println!("Penny value: {}", value_in_cents(Coin::Penny));
}

match with Enums Containing Data

Rust
enum UsState {
    Alabama,
    California,
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Quarter(state) => {
            println!("State quarter from {:?}", state);
            25
        }
        _ => 10,  // Catch-all pattern
    }
}
⚠️

Warning

A match must be exhaustive, every possible value must be covered. Use _ as a catch-all pattern when you don't need to handle every variant individually.

6

The if let Syntax

When you only care about one pattern, if let is more concise than match:

Rust
fn main() {
    let config_max = Some(3u8);
    // Using match (verbose)
    match config_max {
        Some(max) => println!("Maximum is: {}", max),
        _ => {},
    }
    // Using if let (concise)
    if let Some(max) = config_max {
        println!("Maximum is: {}", max);
    }
}

You can also use else with if let:

Rust
fn main() {
    let coin = Coin::Penny;
    if let Coin::Quarter(state) = coin {
        println!("State quarter from {:?}", state);
    } else {
        println!("Not a quarter");
    }
}
💡

Tip

Use if let when you only need to match one pattern. Use match when you need to handle multiple patterns or want the compiler to verify exhaustiveness.

← Previous Chapter 4 of 17 Next →