Skip to Content
Course content

21: Structs and Methods

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're designing a digital camera. A camera isn't just one piece of information; it's a collection of related states. It has an ISO setting, a shutter speed, a battery percentage, and maybe a filename for the next shot. If you tried to track all these as separate variables in your main loop, your code would quickly become a tangled mess of arguments being passed from function to function.

In Rust, a struct is like the physical body of that camera. It lets you bundle all those related pieces of data into a single, named package. But a camera that just sits there holding data isn't very useful. You need buttons to press—actions like "take photo" or "change ISO." That's where methods come in. Methods are functions that "live" inside the struct's implementation, allowing the object to act upon its own data.

Bundling your data together

Let's move away from cameras for a second and look at something we'll actually code. Suppose we're building a game. Instead of having a player_name string and a player_health integer floating around, we'll wrap them in a struct. I prefer this approach because it makes the intent of the code immediately clear to anyone else reading it.

struct Player {
    name: String,
    health: i32,
    level: u32,
}

Now, whenever you need a player, you just create one instance of Player. It's much cleaner than managing three separate variables. I've found that the moment you start grouping data this way, your functions become much simpler because you're passing one "thing" instead of a dozen individual parameters.

Giving your data some behavior

Defining the data is the easy part. The real power comes when we use an impl block. This is where we define the methods that the Player can perform. Unlike the struct definition, which just lists the fields, the impl block is where the logic lives.

impl Player {
    // This is a method. Note the '&self' argument.
    fn shout_name(&self) {
        println!("I am {}!", self.name);
    }

    // This method modifies the player, so we need '&mut self'
    fn take_damage(&mut self, amount: i32) {
        self.health -= amount;
        println!("Ouch! Health is now {}", self.health);
    }
}

Notice that shout_name uses &self. This tells Rust that the method is just borrowing the data to read it. If you want to change a value—like lowering health—you must use &mut self. If you forget the mut, Rust will stop you right there with a compiler error. It's a bit annoying at first, but it saves you from those nightmare bugs where data changes when you didn't expect it to.

The magic of the self keyword

You've probably noticed self appearing everywhere. In Rust, self is how a method accesses the specific instance it was called on. When you call my_player.shout_name(), Rust automatically passes my_player as the first argument to the function.

I should also mention "associated functions." These are functions inside an impl block that don't take self as a parameter. We typically use these as constructors. Since you can't create a struct instance out of thin air without providing all the fields, we usually write a new function to handle the setup logic for us:

impl Player {
    fn new(name: String) -> Self {
        Self {
            name,
            health: 100,
            level: 1,
        }
    }
}

// Now we use it like this:
let mut hero = Player::new(String::from("Althea"));
hero.take_damage(20);

Using Self (with a capital S) is just a shorthand for the name of the struct. It makes your code easier to maintain; if you ever rename Player to Character, you don't have to hunt through every single function to update the return types.




📋 Practical Task

Build a Digital Inventory System

Your task is to create a system for managing an RPG inventory. You need to ensure the system can track items and handle the logic of adding or removing them without letting the inventory count go below zero.

Requirements:

  • Create a struct named Inventory that tracks owner (a String) and item_count (an integer).
  • Implement a new associated function to initialize a new inventory with a name and 0 items.
  • Implement a method add_item that increases the item_count by 1.
  • Implement a method remove_item that decreases the item_count by 1, but only if the count is greater than 0. If it's already 0, it should print "Inventory is empty!" and not subtract anything.
  • In your main function, create an instance of Inventory, add a few items, remove some, and attempt to remove more items than you have to test your logic.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.