Rust - Structs

Overview

Estimated time: 50–70 minutes

Learn how to define and use structs in Rust to create custom data types. Structs allow you to package related data together and define methods to work with that data.

Learning Objectives

Prerequisites

Defining Structs

Structs let you create custom data types by grouping related data together:

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

fn main() {
    let user1 = User {
        username: String::from("alice123"),
        email: String::from("[email protected]"),
        age: 25,
        active: true,
    };
    
    println!("User: {}", user1.username);
    println!("Email: {}", user1.email);
    println!("Age: {}, Active: {}", user1.age, user1.active);
}

Expected output:

User: alice123
Email: [email protected]
Age: 25, Active: true

Mutable Structs

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

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

fn main() {
    let mut point = Point { x: 0, y: 0 };
    
    println!("Initial point: ({}, {})", point.x, point.y);
    
    point.x = 10;
    point.y = 20;
    
    println!("Updated point: ({}, {})", point.x, point.y);
}

Expected output:

Initial point: (0, 0)
Updated point: (10, 20)

Struct Update Syntax

You can create new instances from existing ones using struct update syntax:

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

fn main() {
    let user1 = User {
        username: String::from("alice123"),
        email: String::from("[email protected]"),
        age: 25,
        active: true,
    };
    
    // Create user2 based on user1, but with different email
    let user2 = User {
        email: String::from("[email protected]"),
        ..user1  // Use remaining fields from user1
    };
    
    // Note: user1 is no longer valid because String fields were moved
    println!("User2: {}", user2.username);
    println!("User2 email: {}", user2.email);
}

Expected output:

User2: alice123
User2 email: [email protected]

Tuple Structs

Tuple structs have fields without names, useful for creating distinct types:

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

fn main() {
    let red = Color(255, 0, 0);
    let origin = Point(0, 0, 0);
    
    println!("Red color: ({}, {}, {})", red.0, red.1, red.2);
    println!("Origin point: ({}, {}, {})", origin.0, origin.1, origin.2);
    
    // Color and Point are different types even though they have same structure
    // let mixed = red + origin;  // This would be an error
}

Expected output:

Red color: (255, 0, 0)
Origin point: (0, 0, 0)

Unit-Like Structs

Structs without fields, useful for implementing traits on types without data:

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
    let another = AlwaysEqual;
    
    println!("Created unit-like structs");
    // Useful for implementing traits or as markers
}

Expected output:

Created unit-like structs

Methods

Define methods on structs using impl blocks:

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

impl Rectangle {
    // Method with &self (immutable borrow)
    fn area(&self) -> u32 {
        self.width * self.height
    }
    
    // Method with &mut self (mutable borrow)
    fn double_size(&mut self) {
        self.width *= 2;
        self.height *= 2;
    }
    
    // Method that takes ownership
    fn into_square(self) -> Rectangle {
        let size = std::cmp::max(self.width, self.height);
        Rectangle {
            width: size,
            height: size,
        }
    }
    
    // Method with parameters
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let mut rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    
    println!("Area: {}", rect1.area());
    
    rect1.double_size();
    println!("After doubling: {}x{}", rect1.width, rect1.height);
    
    let rect2 = Rectangle {
        width: 10,
        height: 20,
    };
    
    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    
    let square = rect1.into_square();
    println!("Square: {}x{}", square.width, square.height);
    // rect1 is no longer valid here
}

Expected output:

Area: 1500
After doubling: 60x100
Can rect1 hold rect2? true
Square: 100x100

Associated Functions

Functions associated with a struct but don't take self as a parameter:

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

impl Rectangle {
    // Associated function (constructor)
    fn new(width: u32, height: u32) -> Rectangle {
        Rectangle { width, height }
    }
    
    // Associated function to create a square
    fn square(size: u32) -> Rectangle {
        Rectangle {
            width: size,
            height: size,
        }
    }
    
    // Method
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    // Call associated functions using ::
    let rect1 = Rectangle::new(10, 20);
    let square = Rectangle::square(15);
    
    println!("Rectangle area: {}", rect1.area());
    println!("Square area: {}", square.area());
}

Expected output:

Rectangle area: 200
Square area: 225

Multiple impl Blocks

You can have multiple impl blocks for the same struct:

struct Circle {
    radius: f64,
}

impl Circle {
    fn new(radius: f64) -> Circle {
        Circle { radius }
    }
    
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
}

impl Circle {
    fn circumference(&self) -> f64 {
        2.0 * std::f64::consts::PI * self.radius
    }
    
    fn diameter(&self) -> f64 {
        2.0 * self.radius
    }
}

fn main() {
    let circle = Circle::new(5.0);
    
    println!("Circle with radius: {}", circle.radius);
    println!("Area: {:.2}", circle.area());
    println!("Circumference: {:.2}", circle.circumference());
    println!("Diameter: {:.2}", circle.diameter());
}

Expected output:

Circle with radius: 5
Area: 78.54
Circumference: 31.42
Diameter: 10.00

Practical Example: Bank Account

struct BankAccount {
    account_number: String,
    holder_name: String,
    balance: f64,
}

impl BankAccount {
    fn new(account_number: String, holder_name: String) -> BankAccount {
        BankAccount {
            account_number,
            holder_name,
            balance: 0.0,
        }
    }
    
    fn deposit(&mut self, amount: f64) {
        if amount > 0.0 {
            self.balance += amount;
            println!("Deposited ${:.2}. New balance: ${:.2}", amount, self.balance);
        } else {
            println!("Invalid deposit amount: ${:.2}", amount);
        }
    }
    
    fn withdraw(&mut self, amount: f64) -> bool {
        if amount > 0.0 && amount <= self.balance {
            self.balance -= amount;
            println!("Withdrew ${:.2}. New balance: ${:.2}", amount, self.balance);
            true
        } else {
            println!("Invalid withdrawal: ${:.2} (Balance: ${:.2})", amount, self.balance);
            false
        }
    }
    
    fn get_balance(&self) -> f64 {
        self.balance
    }
    
    fn account_info(&self) {
        println!("Account: {}", self.account_number);
        println!("Holder: {}", self.holder_name);
        println!("Balance: ${:.2}", self.balance);
    }
}

fn main() {
    let mut account = BankAccount::new(
        String::from("ACC123456"),
        String::from("Alice Johnson")
    );
    
    account.account_info();
    
    account.deposit(1000.0);
    account.deposit(250.50);
    
    account.withdraw(300.0);
    account.withdraw(2000.0);  // Should fail
    
    println!("Final balance: ${:.2}", account.get_balance());
}

Expected output:

Account: ACC123456
Holder: Alice Johnson
Balance: $0.00
Deposited $1000.00. New balance: $1000.00
Deposited $250.50. New balance: $1250.50
Withdrew $300.00. New balance: $950.50
Invalid withdrawal: $2000.00 (Balance: $950.50)
Final balance: $950.50

Common Pitfalls

1. Forgetting to Make Struct Mutable

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

fn main() {
    // Wrong: immutable struct
    // let point = Point { x: 0, y: 0 };
    // point.x = 10;  // Error!
    
    // Correct: mutable struct
    let mut point = Point { x: 0, y: 0 };
    point.x = 10;  // OK
    println!("Point: ({}, {})", point.x, point.y);
}

2. Moving Ownership with Struct Update

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

fn main() {
    let user1 = User {
        name: String::from("Alice"),
        age: 25,
    };
    
    let user2 = User {
        age: 30,
        ..user1  // user1.name is moved here
    };
    
    // println!("{}", user1.name);  // Error: value borrowed after move
    println!("{}", user2.name);  // OK
}

3. Method vs Associated Function Syntax

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

impl Rectangle {
    fn new(width: u32, height: u32) -> Rectangle {
        Rectangle { width, height }
    }
    
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    // Associated function: use ::
    let rect = Rectangle::new(10, 20);
    
    // Method: use .
    let area = rect.area();
    
    println!("Area: {}", area);
}

Checks for Understanding

Question 1: What's the difference between a method and an associated function?

Answer: A method takes &self, &mut self, or self as the first parameter and is called with dot notation (e.g., instance.method()). An associated function doesn't take self and is called with :: notation (e.g., Type::function()).

Question 2: Why might this code not compile?
struct Point { x: i32, y: i32 }
let point = Point { x: 0, y: 0 };
point.x = 10;

Answer: The point variable is immutable. To modify struct fields, the entire struct instance must be declared as mutable: let mut point = Point { x: 0, y: 0 };

Question 3: How would you create a constructor function for this struct?
struct Person {
    name: String,
    age: u32,
}

Answer:

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

← PreviousNext →