DaleSchool

Creating Your Own Types: Structs

Intermediate20min

Learning Objectives

  • Create custom types with struct
  • Access and modify struct fields
  • Add methods to a struct with impl blocks
  • Understand the difference between &self and &mut self

Working Code

Imagine storing user information: name, age, and email. Until now you'd need three separate variables. With a struct, you can bundle them into one:

#[derive(Debug)]
struct User {
    name: String,
    age: u32,
    email: String,
}

fn main() {
    let user = User {
        name: String::from("Alice"),
        age: 25,
        email: String::from("alice@example.com"),
    };

    println!("{:?}", user);
    println!("Name: {}", user.name);
}

Adding #[derive(Debug)] lets you print the entire struct with {:?}. Don't worry about the details yet — for now, think of it as "magic that enables debug output."

Try It Yourself

  1. Add an is_active: bool field to User and set it to true.
  2. Try modifying user.name with user.name = String::from("Bob");. What error do you get?

"Why?" — Structs and Ownership

Structs are an extension of ownership. When a User owns a String field, the struct is the owner of that string. When the struct is dropped, its field values are cleaned up too.

Modifying Fields

To change a struct's values, the variable itself must be mut. Rust doesn't allow individual fields to be mutable — the whole struct is mutable or the whole struct is immutable.

#[derive(Debug)]
struct User {
    name: String,
    age: u32,
}

fn main() {
    let mut user = User {
        name: String::from("Alice"),
        age: 25,
    };

    user.age = 26;  // OK because it's mut!
    println!("{:?}", user);
}

Adding Methods — impl Blocks

To give a struct behavior, use an impl block:

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

impl Rectangle {
    // Method: borrows self immutably
    fn area(&self) -> f64 {
        self.width * self.height
    }

    fn is_square(&self) -> bool {
        self.width == self.height
    }

    // Associated function: no self (constructor pattern)
    fn square(size: f64) -> Rectangle {
        Rectangle {
            width: size,
            height: size,
        }
    }
}

fn main() {
    let rect = Rectangle { width: 10.0, height: 5.0 };
    println!("Area: {}", rect.area());
    println!("Square? {}", rect.is_square());

    let sq = Rectangle::square(7.0);
    println!("Square area: {}", sq.area());
}

&self, &mut self, self — Borrowing Rules Apply

The first parameter of a method determines how self is used. It follows the same borrowing rules from previous lessons!

| Parameter | Meaning | Analogy | | ----------- | ---------------------------- | ------------------------------------ | | &self | Read-only (immutable borrow) | Reading a library book | | &mut self | Can modify (mutable borrow) | Editing with write permission | | self | Takes ownership | Taking the book and not returning it |

#[derive(Debug)]
struct Counter {
    count: i32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }

    fn value(&self) -> i32 {
        self.count          // read-only
    }

    fn increment(&mut self) {
        self.count += 1;    // modify
    }
}

fn main() {
    let mut c = Counter::new();
    c.increment();
    c.increment();
    println!("Count: {}", c.value());
}

increment modifies the value, so it uses &mut self. value only reads, so it uses &self.

Learn More: Putting &str in a Struct

If you try to put &str in a struct field, you get this error:

struct User {
    name: &str,  // error!
}
error[E0106]: missing lifetime specifier

To store a borrowed value (&str) in a struct, you must specify a lifetime — telling the compiler "how long this borrowed value stays valid."

Lifetimes are covered in Lesson 15. For now, remember that using String for struct fields is the safe default!

Exercise 1. Create a Book struct and add a summary method.

// TODO: Define Book struct (title: String, author: String, pages: u32)
// TODO: Add #[derive(Debug)]
// TODO: Add a summary method in an impl block
//       Return a String like "《Title》 - Author (N pages)"

fn main() {
    let book = Book {
        title: String::from("Rust Programming"),
        author: String::from("Dale"),
        pages: 300,
    };
    println!("{}", book.summary());
    // Output: 《Rust Programming》 - Dale (300 pages)
}

Hint: The format! macro comes in handy.

Exercise 2. Add a reset method to the Counter struct. It should set count back to 0.

impl Counter {
    // ... existing methods ...

    fn reset(&mut self) {
        // Complete this
    }
}

Hint: Since you're modifying a value, what kind of self do you need?

Q1. How do you make struct fields modifiable?

  • A) Put mut before the field name
  • B) Declare the variable with let mut
  • C) Add #[derive(Mutable)]
  • D) Put mut before the field type

Q2. What does &self mean in an impl block?

  • A) It takes ownership of the struct
  • B) It borrows the struct immutably (read-only)
  • C) It borrows the struct mutably (can modify)
  • D) It creates a new struct

Q3. What do you call a function like Rectangle::square(5.0) that's called without self?

  • A) A method
  • B) An associated function
  • C) A closure
  • D) A macro