Attaching code to types

If you want to add behaviour or other associated code to your types, you can do so by writing an impl block on that type. Such a block can contain associated constants, types and functions. There can also be multiple impl blocks for the same type, for example in different files.

Methods that are called on an object instance are written as a regular function, but take some form of self as their first parameter, meaning they need an object instance to be called. In contrast, functions without a self parameter can only be called on the type, not on an instance. Object methods can also be called like a normal associated function, but need an object instance for their first parameter in that case, similar to how C++ handles things.

Additionally to the self keyword, which translates to this in many other languages, there is also a Self keyword with a capitalized S. This doesn't refer to an object instance, but rather to the type which is implemented in the current impl block. It can be used instead of the explicit type to make the relation more clear. For example, a constructor typically is a stock standard function that does not use a self parameter, but returns a Self object.

In contrast, self can't be replaced by anything else and fields or methods of the objects can only be accessed explicitly through it. For people who don't have all fields and method names in their head, this makes it very easy to identify those elements. It also helps to discern them from external constants or functions and local variables.

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

impl Rectangle {
  const MAX_HEIGHT: usize = 42;
  // Notice the capitalized 'Self' as type
  const EXAMPLE: Self = Self {
    width: 2,
    height: 1,
  };

  // Default constructor, no special case in the language
  pub fn new() -> Self {
    return Self::EXAMPLE;
  }

  pub fn associated_function() -> usize {
    return Self::MAX_HEIGHT;
  }

  // Non-capitalized 'self' as object instance
  pub fn object_method(self) -> usize {
    return self.width;
  }
}

fn example() {
  let object: Rectangle = Rectangle::new();
  let another_object: Rectangle = Rectangle::new();
  println!("{}", Rectangle::associated_function());
  println!("{}", object.object_method());
  // Calling a method as associated function works, too!
  println!("{}", Rectangle::object_method(another_object));
}
fn main() { example(); }

Now traits make implementations much more flexible still, but more on that, you guessed it, in its own chapter.