RustMethods & Associated Functions

Methods and Associated Functions

Defining a struct gives you a data type. To give that type behavior you write an impl block. Inside an impl block you can define two kinds of items: methods (which operate on an instance) and associated functions (which belong to the type itself but do not need an instance).

The impl Block

Everything related to a type's behavior lives inside one or more impl blocks. The block starts with impl TypeName followed by curly braces:

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

impl Rectangle {
    // methods and associated functions go here
}
Note
Rust allows multiple `impl` blocks for the same type. They are all merged together by the compiler — useful when a type is large or when you want to group related methods into separate blocks.
Methods with &self

The most common kind of method borrows the instance immutably using &self. Use this when your method only needs to read data from the struct:

RUST
impl Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }

    fn perimeter(&self) -> f64 {
        2.0 * (self.width + self.height)
    }

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

let rect = Rectangle { width: 10.0, height: 5.0 };

println!("Area:      {}", rect.area());
println!("Perimeter: {}", rect.perimeter());
println!("Square?    {}", rect.is_square());
Tip
`&self` is shorthand for `self: &Self`. Rust automatically borrows the receiver when you call a method — you never need to write `rect.area(&rect)` manually.
Methods with &mut self

When a method needs to change one or more fields of the struct, use &mut self. The instance must also be declared mut at the call site:

RUST
impl Rectangle {
    fn scale(&mut self, factor: f64) {
        self.width  *= factor;
        self.height *= factor;
    }

    fn set_width(&mut self, w: f64) {
        self.width = w;
    }
}

let mut rect = Rectangle { width: 4.0, height: 3.0 };

println!("Before: {:?}", rect);
rect.scale(2.0);
println!("After:  {:?}", rect);
Methods with self (Taking Ownership)

A method can take ownership of the instance by using self (no &). After the method call the original variable is moved and can no longer be used. This is uncommon but useful for consuming builder types or converting between types:

RUST
impl Rectangle {
    fn into_square(self) -> Rectangle {
        let side = f64::min(self.width, self.height);
        Rectangle { width: side, height: side }
    }
}

let rect = Rectangle { width: 10.0, height: 4.0 };
let square = rect.into_square();

// println!("{:?}", rect); // compile error — rect was moved
println!("Square: {:?}", square);
Warning
Taking `self` by value consumes the instance. The caller can no longer use it after the method call. Reserve this for transformation methods where the old value is intentionally replaced.
Associated Functions

Associated functions do not take self as their first parameter — they are called on the type, not on an instance, using the :: syntax. The most common use is as a constructor:

RUST
impl Rectangle {
    // Associated function — no self parameter
    fn new(width: f64, height: f64) -> Rectangle {
        Rectangle { width, height }
    }

    fn square(size: f64) -> Rectangle {
        Rectangle { width: size, height: size }
    }
}

let rect   = Rectangle::new(10.0, 5.0);
let square = Rectangle::square(7.0);

println!("rect:   {:?}", rect);
println!("square: {:?}", square);
Note
By convention, `new` is the standard constructor name in Rust — but it is just a regular associated function. Rust has no built-in `new` keyword.
The Self Type Alias

Inside an impl block, Self (capital S) is an alias for the type being implemented. Using Self makes code easier to refactor when you rename a type:

RUST
impl Rectangle {
    fn new(width: f64, height: f64) -> Self {
        Self { width, height }
    }

    fn clone_rect(&self) -> Self {
        Self {
            width: self.width,
            height: self.height,
        }
    }
}
Automatic Referencing and Dereferencing

Rust automatically adds the right &, &mut, or * when you call a method. The following three calls are equivalent — Rust figures out which one to use based on what the method signature requires:

RUST
let rect = Rectangle { width: 10.0, height: 5.0 };

// All three are identical — Rust inserts the & automatically
let a = rect.area();
let b = (&rect).area();
// let c = Rectangle::area(&rect); // explicit form — less common
Tip
This automatic referencing is why method call syntax (`rect.area()`) feels more ergonomic than function call syntax (`Rectangle::area(&rect)`). Both work, but the dot syntax is idiomatic Rust.
Getters and Setters

Because struct fields are private by default (within a module), getters and setters are the standard way to expose controlled access to internal state:

RUST
pub struct Circle {
    radius: f64,  // private field
}

impl Circle {
    pub fn new(radius: f64) -> Self {
        assert!(radius > 0.0, "Radius must be positive");
        Self { radius }
    }

    // getter
    pub fn radius(&self) -> f64 {
        self.radius
    }

    // setter with validation
    pub fn set_radius(&mut self, r: f64) {
        assert!(r > 0.0, "Radius must be positive");
        self.radius = r;
    }

    pub fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
}

let mut c = Circle::new(5.0);
println!("Radius: {}", c.radius());
println!("Area:   {:.2}", c.area());

c.set_radius(10.0);
println!("New area: {:.2}", c.area());
The Builder Pattern

When a type has many optional fields the builder pattern keeps construction readable. Each setter method returns &mut Self so calls can be chained:

RUST
#[derive(Debug)]
struct QueryBuilder {
    table: String,
    limit: u32,
    offset: u32,
    order_by: Option<String>,
}

impl QueryBuilder {
    fn new(table: &str) -> Self {
        Self {
            table: table.to_string(),
            limit: 100,
            offset: 0,
            order_by: None,
        }
    }

    fn limit(&mut self, n: u32) -> &mut Self {
        self.limit = n;
        self
    }

    fn offset(&mut self, n: u32) -> &mut Self {
        self.offset = n;
        self
    }

    fn order_by(&mut self, col: &str) -> &mut Self {
        self.order_by = Some(col.to_string());
        self
    }

    fn build(&self) -> String {
        let order = self.order_by
            .as_deref()
            .unwrap_or("id");
        format!(
            "SELECT * FROM {} ORDER BY {} LIMIT {} OFFSET {}",
            self.table, order, self.limit, self.offset
        )
    }
}

let query = QueryBuilder::new("users")
    .limit(10)
    .offset(20)
    .order_by("name")
    .build();

println!("{}", query);
Multiple impl Blocks

A single type can have multiple impl blocks. Rust merges them all. This is helpful for organizing a large type or when generic bounds differ between groups of methods:

RUST
#[derive(Debug)]
struct Matrix {
    data: Vec<Vec<f64>>,
    rows: usize,
    cols: usize,
}

// Block 1: constructors
impl Matrix {
    fn zeros(rows: usize, cols: usize) -> Self {
        Self {
            data: vec![vec![0.0; cols]; rows],
            rows,
            cols,
        }
    }
}

// Block 2: inspection methods
impl Matrix {
    fn rows(&self) -> usize { self.rows }
    fn cols(&self) -> usize { self.cols }

    fn get(&self, r: usize, c: usize) -> f64 {
        self.data[r][c]
    }
}

// Block 3: mutation methods
impl Matrix {
    fn set(&mut self, r: usize, c: usize, val: f64) {
        self.data[r][c] = val;
    }
}

let mut m = Matrix::zeros(2, 3);
m.set(0, 1, 42.0);
println!("rows={} cols={}", m.rows(), m.cols());
println!("m[0][1] = {}", m.get(0, 1));
Methods That Compare Instances

A method can take another instance of the same type as a parameter, which is useful for comparisons or combinations:

RUST
impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }

    fn total_area_with(&self, other: &Rectangle) -> f64 {
        self.area() + other.area()
    }
}

let big   = Rectangle { width: 20.0, height: 10.0 };
let small = Rectangle { width: 5.0,  height: 3.0  };

println!("big can hold small: {}", big.can_hold(&small));
println!("small can hold big: {}", small.can_hold(&big));
println!("combined area: {}", big.total_area_with(&small));
Quick Reference: self Variants

Parameter

Ownership

Mutates instance?

Typical use

&self

Borrows immutably

No

Reading data, calculations

&mut self

Borrows mutably

Yes

Modifying fields, setters

self

Takes ownership

Yes (then dropped)

Consuming transformations

Full Example

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

impl User {
    // Associated function — constructor
    fn new(name: &str, email: &str, age: u32) -> Self {
        Self {
            name: name.to_string(),
            email: email.to_string(),
            age,
            active: true,
        }
    }

    // Immutable borrow — reading
    fn display_name(&self) -> &str {
        &self.name
    }

    fn is_adult(&self) -> bool {
        self.age >= 18
    }

    // Mutable borrow — modifying
    fn birthday(&mut self) {
        self.age += 1;
        println!("Happy birthday, {}! You are now {}.", self.name, self.age);
    }

    fn deactivate(&mut self) {
        self.active = false;
    }

    // Takes ownership — consuming conversion
    fn into_summary(self) -> String {
        format!("{} <{}> age {}", self.name, self.email, self.age)
    }
}

fn main() {
    let mut user = User::new("Alice", "alice@example.com", 29);

    println!("Name:  {}", user.display_name());
    println!("Adult: {}", user.is_adult());

    user.birthday();

    let summary = user.into_summary();
    // user is moved — cannot use it here
    println!("Summary: {}", summary);
}
Success
You now understand `impl` blocks, the three `self` variants, associated functions, the builder pattern, and how Rust automatically references method calls. Next, explore enums — which pair beautifully with structs and unlock pattern matching.