Implementing methods

Methods are functions tied to a specific context, such as a struct, enum, or trait object. They can access the data of their parent type, utilize it for various operations, or modify it as needed. This allows for more organized and encapsulated code, making it easier to manage and maintain.

Learn Rust by Practice

Master Rust through hands-on coding exercises and real-world examples.

Defining methods

Methods are defined in the same way as functions, but with an additional first parameter self. This self parameter is either an owned value, an immutable reference, or a mutable reference to the parent type (in this case, a struct).

Let's stay we have a video game character named Dragon with the following properties:

struct Dragon {
    name: String,
    level: u32,
    fire_power: u32,
}

In a video game context, the dragon might have some properties like name, level, and fire_power. Which we defined in the struct above as named fields.

A dragon can perform a few actions, like attacking, leveling up, moving and so on. These actions depend on the already existing data of the dragon, for example, we need to know the existing level of the dragon to know which level it should level up to.

Let's write a few methods, one will take an immutable reference, and one will take a mutable reference.

Method with immutable reference to self

If a method doesn't need to mutate any data on the struct, we'll use an immutable reference to self. This is done by using &self as the first parameter of the method.

impl Dragon {
    fn make_sound(&self) {
        println!("{} roars!", self.name);
    }
}

The syntax of implementing method is similar to defining functions but inside a block of impl with the name of the struct.

Method with mutable reference to self

Let's implement the level_up() method to level up the dragon. This method will take a mutable reference to self using &mut self.

impl Dragon {
  fn level_up(&mut self) {
      self.level += 1;
      self.fire_power += 10;
 
      println!("{} leveled up to level {}!", self.name, self.level);
  }
}

The level_up() method takes a mutable reference to self using &mut self. This allows the method to modify the fields of the struct, in this case it's incrementing the level and fire_power of the dragon.

The &mut self parameter is just a shorthand for dragon: &mut Dragon, since this is a common pattern, Rust provides a shorthand syntax to make it easier to work with methods.

You can implement multiple methods inside the same impl block, each method can have a different type of self parameter. Let's define another method in the same impl block.

impl Dragon {
  fn level_up(&mut self) {
    self.level += 1;
    self.fire_power += 10;
 
    println!("{} leveled up to level {}!", self.name, self.level);
  }
 
  fn make_sound(&self) {
    println!("{} roars!", self.name);
  }
 
  fn attack(&self) {
    println!("{} attacks with a fireball!", self.name);
  }
}

Returning values from methods

You can also return values from methods. Just like functions, the return type of the function must be explicitly specified.

Let's add a return type of (u32, u32) to the level_up() method to return the new level and fire power of the dragon.

impl Dragon {
  fn level_up(&mut self) -> (u32, u32) {
    self.level += 1;
    self.fire_power += 10;
 
    println!("{} leveled up to level {}!", self.name, self.level);
 
    (self.level, self.fire_power)
  }
 
  fn make_sound(&self) {
    println!("{} roars!", self.name);
  }
 
  fn attack(&self) {
    println!("{} attacks with a fireball!", self.name);
  }
}

Using methods

Now that we have defined the methods, let's see them in action. We have a dragon named Mushu which is at level 1 and has a fire_power of 10, he does a few attacks and then levels up.

fn main() {
    let mut mushu = Dragon {
        name: "Mushu".to_string(),
        level: 1,
        fire_power: 10,
    };
 
    println!("{:?}", mushu);
 
    mushu.attack();
    mushu.attack();
 
    mushu.level_up();
    println!("{:?}", mushu);
}

When we run the code, we get the following output:

Dragon { name: "Mushu", level: 1, fire_power: 10 }
Mushu attacks with a fireball!
Mushu attacks with a fireball!
Mushu leveled up to level 2!
Dragon { name: "Mushu", level: 2, fire_power: 20 }

As you can see, we were able to call the methods on the mushu instance of the Dragon struct. The methods were able to access the fields of the struct and modify them as needed.

Associated functions

In addition to methods, Rust also has associated functions. These are functions that are defined on the struct itself and they do not take any reference to the struct as a parameter nor do they take ownership of the struct, they can be called without an instance of the struct.

Unlike methods, associated functions are called by using the :: syntax on the struct itself, not on an instance of the struct. They are similar to static methods in other languages.

The most commonly used associated function in rust is the conventional new function that is used to create a new instance of the struct, in other languages, this is usually a constructor, Rust doesn't have a constructor but instead you can define your own associated function to create a new instance of the struct.

You can define an associated function using the impl block, just like you do with methods. The only difference is that you don't need to use &self or &mut self as the first parameter.

Let's add an associated function to the Dragon struct that creates a new dragon with a given name:

impl Dragon {
  fn new(name: &str) -> Self {
    Self {
        name: name.to_string(),
        level: 1,
        fire_power: level * 10,
    }
  }
}

The function above is not called a method but an associated function, it doesn't take any reference to any instance of the struct.

fn main() {
    let new_dragon = Dragon::new("Smaug");
    println!("{:?}", new_dragon);
}

When we run the code, we get the following output:

Dragon { name: "Smaug", level: 1, fire_power: 10 }

Conclusion

In this lesson, we learned about methods and associated functions in Rust, how to define them and work with them.

Methods are functions that are tied to a specific context like a struct. They can access the data of their parent type, utilize it for various operations, or modify it as needed.

Associated functions on the other hand are functions that are defined on the struct itself and they do not take any reference to the struct as a parameter nor do they take ownership of the struct, they can be called without an instance of the struct and they're most commonly used to create a new instance of the struct, it's also a convention in Rust to name the constructor function new.

In the next lesson, we have a challenge for you to help you practice what you've learned about defining methods and associated functions on structs in Rust.

Go to the next lesson when you're ready to take on the challenge. Good Luck!