Beginner Level

Structs

Chapter 6: Structs šŸ—ļø

Structs let you create custom data types that group related data together. They're fundamental to organizing code in Rust!

Defining and Instantiating Structs

Basic Struct Definition

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

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

Mutable Structs

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

    user1.email = String::from("anotheremail@example.com");
}

Using the Field Init Shorthand

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

Struct Update Syntax

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

    let user2 = User {
        email: String::from("another@example.com"),
        ..user1  // Use remaining fields from user1
    };
    
    // Note: user1 can no longer be used because username was moved
}

Tuple Structs

Structs that look like 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!("Black: ({}, {}, {})", black.0, black.1, black.2);
}

Unit-Like Structs

Structs without any fields:

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

Method Syntax

Defining Methods

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

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
    
    fn width(&self) -> bool {
        self.width > 0
    }
    
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

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

    println!("The area of the rectangle is {} square pixels.", rect1.area());
    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
}

Associated Functions

Functions that don't take self as a parameter:

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

fn main() {
    let sq = Rectangle::square(3);
}

Multiple impl Blocks

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

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

Practical Examples

Example 1: Person Struct

#[derive(Debug)]
struct Person {
    first_name: String,
    last_name: String,
    age: u32,
}

impl Person {
    fn new(first_name: String, last_name: String, age: u32) -> Self {
        Self {
            first_name,
            last_name,
            age,
        }
    }
    
    fn full_name(&self) -> String {
        format!("{} {}", self.first_name, self.last_name)
    }
    
    fn is_adult(&self) -> bool {
        self.age >= 18
    }
    
    fn have_birthday(&mut self) {
        self.age += 1;
    }
}

fn main() {
    let mut person = Person::new(
        String::from("John"),
        String::from("Doe"),
        25
    );
    
    println!("Full name: {}", person.full_name());
    println!("Is adult: {}", person.is_adult());
    
    person.have_birthday();
    println!("New age: {}", person.age);
}

Example 2: Bank Account

#[derive(Debug)]
struct BankAccount {
    account_number: String,
    holder_name: String,
    balance: f64,
}

impl BankAccount {
    fn new(account_number: String, holder_name: String) -> Self {
        Self {
            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);
        }
    }
    
    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!("Insufficient funds or invalid amount");
            false
        }
    }
    
    fn get_balance(&self) -> f64 {
        self.balance
    }
}

fn main() {
    let mut account = BankAccount::new(
        String::from("123456789"),
        String::from("Alice Smith")
    );
    
    account.deposit(1000.0);
    account.withdraw(250.0);
    println!("Final balance: ${:.2}", account.get_balance());
}

Ownership and Structs

Structs with References (Preview)

struct User<'a> {
    username: &'a str,
    email: &'a str,
    sign_in_count: u64,
    active: bool,
}

Note: This requires lifetimes, which we'll cover in Chapter 13.

Debugging Structs

Using Debug Trait

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

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {:?}", rect1);
    println!("rect1 is {:#?}", rect1);  // Pretty print
}

Practice Exercises

Exercise 1: Student Grade System

#[derive(Debug)]
struct Student {
    name: String,
    grades: Vec<f64>,
}

impl Student {
    fn new(name: String) -> Self {
        // Your implementation
    }
    
    fn add_grade(&mut self, grade: f64) {
        // Your implementation
    }
    
    fn average_grade(&self) -> f64 {
        // Your implementation
    }
    
    fn letter_grade(&self) -> char {
        // Your implementation
    }
}

Exercise 2: Point and Distance

#[derive(Debug)]
struct Point {
    x: f64,
    y: f64,
}

impl Point {
    fn new(x: f64, y: f64) -> Self {
        // Your implementation
    }
    
    fn distance_from_origin(&self) -> f64 {
        // Your implementation
    }
    
    fn distance_from(&self, other: &Point) -> f64 {
        // Your implementation
    }
}

Common Patterns

Builder Pattern (Preview)

struct User {
    name: String,
    email: String,
    age: Option<u32>,
}

impl User {
    fn builder() -> UserBuilder {
        UserBuilder::default()
    }
}

#[derive(Default)]
struct UserBuilder {
    name: Option<String>,
    email: Option<String>,
    age: Option<u32>,
}

impl UserBuilder {
    fn name(mut self, name: String) -> Self {
        self.name = Some(name);
        self
    }
    
    fn email(mut self, email: String) -> Self {
        self.email = Some(email);
        self
    }
    
    fn age(mut self, age: u32) -> Self {
        self.age = Some(age);
        self
    }
    
    fn build(self) -> Result<User, &'static str> {
        Ok(User {
            name: self.name.ok_or("Name is required")?,
            email: self.email.ok_or("Email is required")?,
            age: self.age,
        })
    }
}

Key Takeaways

  • āœ… Structs group related data together
  • āœ… Use impl blocks to define methods and associated functions
  • āœ… Methods take &self, &mut self, or self as first parameter
  • āœ… Associated functions don't take self (like constructors)
  • āœ… #[derive(Debug)] enables debug printing
  • āœ… Field init shorthand reduces repetition

Ready for Chapter 7? → Enums and Pattern Matching

šŸ¦€ Rust Programming Tutorial

Learn from Zero to Advanced

Built with Next.js and Tailwind CSS • Open Source