8.12 Methods and Associated Functions

Rust allows associating functions directly with structs, enums, and traits using impl (implementation) blocks. These associated functions come in two main forms: methods and associated functions (often called “static methods” in other languages).

  • Methods: Functions that operate on an instance of a type. Their first parameter is always written as self, &self, or &mut self. These represent the instance itself, an immutable borrow of the instance, or a mutable borrow, respectively. Methods are called using dot notation (instance.method()).
    • Note on Self Type: These parameter forms (self, &self, &mut self) are actually shorthand for self: Self, self: &Self, and self: &mut Self. Here, Self (capital ‘S’) is a special type alias within an impl block that refers to the type the block is implementing (e.g., Circle within impl Circle { ... }). This shows that self parameters still follow the standard parameter: Type syntax.
  • Associated Functions: Functions associated with a type but not tied to a specific instance. They do not take self as the first parameter. They are called using the type name and :: syntax (Type::function()). They are commonly used for constructors or utility functions related to the type.

8.12.1 Defining and Calling Methods and Associated Functions

struct Circle {
    radius: f64,
}

// Implementation block for the Circle struct (Here, Self = Circle)
impl Circle {
    // Associated function: often used as a constructor.
    // Does not take 'self'. Called using Circle::new(...).
    pub fn new(radius: f64) -> Self { // 'Self' refers to the type 'Circle'
        if radius < 0.0 {
            panic!("Radius cannot be negative");
        }
        Circle { radius }
    }

    // Method: takes an immutable reference ('self: &Self').
    // Called using my_circle.area().
    pub fn area(&self) -> f64 { // Short for 'self: &Self' or 'self: &Circle'
        std::f64::consts::PI * self.radius * self.radius
    }

    // Method: takes a mutable reference ('self: &mut Self').
    // Called using my_circle.scale(...).
    pub fn scale(&mut self, factor: f64) { // Short for 'self: &mut Self' or 'self: &mut Circle'
        if factor < 0.0 {
            panic!("Scale factor cannot be negative");
        }
        self.radius *= factor;
    }

    // Method: takes ownership ('self: Self').
    // Called using my_circle.consume(). The instance cannot be used afterwards.
    pub fn consume(self) { // Short for 'self: Self' or 'self: Circle'
        println!("Consuming circle with radius {}", self.radius);
        // 'self' (the circle instance) is dropped here.
    }
}

fn main() {
    // Call associated function (constructor)
    let mut my_circle = Circle::new(5.0);
    // Call methods using dot notation
    println!("Initial Area: {}", my_circle.area());
    my_circle.scale(2.0); // Calls the mutable method
    println!("Scaled Radius: {}", my_circle.radius);
    println!("Scaled Area: {}", my_circle.area());
    // Call method that consumes the instance
    // my_circle.consume();
    // println!("Area after consume: {}", my_circle.area());
    // Error: use of moved value 'my_circle'

    // Alternative way to call methods (less common):
    // Explicitly pass the instance reference.
    let radius = 10.0;
    let another_circle = Circle::new(radius);
    let area = Circle::area(&another_circle); // Equivalent to another_circle.area()
    println!("Area of another circle: {}", area);
}

As noted in Section 6.3.1, Rust performs automatic referencing and dereferencing for method calls. When using the dot operator (object.method()), the compiler automatically inserts the appropriate &, &mut, or * to match the method’s self, &self, or &mut self receiver as required.