Method Syntax
How to define and call methods on structs in Rust using impl blocks, including self parameters, associated functions, and automatic referencing.
How Methods Differ from Free Functions
A free function takes all the data it needs as explicit arguments. If you have a Rectangle struct and you want to compute its area, you might write fn area(rect: &Rectangle) -> u32 and call it with area(&rect). That works, but it scatters the operations related to Rectangle across the codebase.
A method is a function whose first parameter is always self, written inside an impl block for the type. It receives the instance automatically when called with the dot notation: rect.area(). The dot call passes the instance behind the scenes, so you never have to pass it again. This groups behaviour with the data it operates on and makes the calling code read naturally.
Defining Methods with impl
To add methods to a struct you open an impl block. The keyword impl stands for “implementation”. Every function you write inside that block becomes an associated function of the struct, and those that take self are its methods.
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect = Rectangle { width: 30, height: 50 };
println!("The area is {} square pixels.", rect.area());
}
Inside impl Rectangle, Self (with a capital S) is an alias for Rectangle. That means you can write &self as shorthand for self: &Self, which is self: &Rectangle. The method body accesses fields through self, just as you would through any other reference to the struct.
The self Parameter and Its Variants
The first parameter of a method controls whether the method reads, modifies, or consumes the instance:
&self– an immutable borrow. Use this when the method only needs to read data from the struct. This is the most common form.&mut self– a mutable borrow. Use this when the method must change one or more fields.self– takes ownership of the instance. The caller loses the instance after the call. This is rare and typically used when the method transforms the instance into something else, so the old one should no longer exist.
impl Rectangle {
// reads, no mutation
fn area(&self) -> u32 { self.width * self.height }
// changes a field
fn double_size(&mut self) {
self.width *= 2;
self.height *= 2;
}
// consumes the rectangle and returns a new string
fn to_label(self) -> String {
format!("Rectangle {}x{}", self.width, self.height)
}
}
Accidental Move with self:
A method that takes self (ownership) will move the value. After calling such a method, the original variable is no longer usable. This often surprises beginners who add a method like fn finalize(self) and then try to use the instance again. Always use &self or &mut self unless you genuinely need to consume the struct.
A Basic Example — Computing Area
The area method above demonstrates the core pattern. It takes &self because multiplication doesn’t need to modify the fields. The call rect.area() is functionally identical to calling a function with a reference, but the method syntax feels more natural and keeps the Rectangle operations together.
Methods with Multiple Parameters
Methods can have additional parameters after self. Consider a can_hold method that checks whether one rectangle can completely contain another:
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let rect1 = Rectangle { width: 60, height: 45 };
let rect2 = Rectangle { width: 20, height: 30 };
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
}
The call rect1.can_hold(&rect2) passes rect1 as &self and &rect2 as the other parameter. Everything after self works exactly like regular function parameters.
Automatic Referencing and Dereferencing
Rust does not require the -> operator that C and C++ use for calling methods on pointers. Instead, when you write object.method(), Rust automatically inserts &, &mut, or * as needed so that the receiver matches the method signature. Consider this:
impl Point {
fn distance(&self, other: &Point) -> f64 {
let x_sq = (other.x - self.x).powi(2);
let y_sq = (other.y - self.y).powi(2);
(x_sq + y_sq).sqrt()
}
}
let p1 = Point { x: 0.0, y: 0.0 };
let p2 = Point { x: 5.0, y: 6.5 };
// Both calls work identically:
p1.distance(&p2);
(&p1).distance(&p2);
Because the method signature declares &self, Rust can accept a direct p1 and automatically borrow it. This mechanism eliminates the manual dereferencing noise that plagues pointer-heavy code and makes ownership feel ergonomic.
Ergonomic Borrowing:
Automatic referencing works because Rust knows exactly which variant of self the method expects. The compiler figures out whether to add a & or &mut for you, so you can write clean, concise code without losing any safety guarantees.
Methods vs. Associated Functions
Not every function inside an impl block needs self. A function that doesn’t take self is still associated with the type, but it is not a method — you call it with the :: syntax on the type name, not with a dot on an instance.
Associated functions are often used as constructors, providing a convenient way to create instances.
Constructors and the new Convention
Rust does not have a special constructor keyword, but the community has adopted the name new for functions that return a new instance. Here is an associated function that creates a square Rectangle:
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
let sq = Rectangle::square(4);
Self in the return type and body again means Rectangle. Calling Rectangle::square(4) creates a new value without needing any existing instance. This is exactly how String::from("hello") works — from is an associated function, not a method.
Self as Return Type:
Using Self instead of the concrete type name in the return type makes refactoring easier. If the struct name changes later, the impl block remains correct.
Multiple impl Blocks
A struct can have more than one impl block. Splitting methods across multiple blocks does not affect behaviour; everything still belongs to the same type.
impl Rectangle {
fn area(&self) -> u32 { self.width * self.height }
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
For a simple struct like Rectangle, splitting is unnecessary. However, multiple impl blocks become useful with generic types and traits. In Chapter 10, you’ll see that you can have separate impl blocks for different trait implementations, keeping concerns cleanly separated.
Getters and Method–Field Name Overlap
Rust allows a method to share the same name as a field. The compiler distinguishes them by syntax: rect.width (no parentheses) accesses the field, while rect.width() calls the method.
A common pattern is to create a getter method that simply returns the field’s value:
impl Rectangle {
fn width(&self) -> u32 {
self.width
}
}
This might look redundant, but it becomes important when a field is private. If width is not visible outside the module, the width() method can expose read‑only access without letting anyone modify the field directly. Getters are one of Rust’s encapsulation tools.
Private Fields, Public Getter:
If you define a getter but make the field itself public, external code can still mutate the field directly. To enforce read‑only access, use a private field and only expose the getter method. Public/private visibility is covered in Chapter 7.
Method Calls as Syntactic Sugar
Under the hood, a method call is syntax sugar for an associated function call with the instance passed as the first argument. For example:
let mut r = Rectangle { width: 10, height: 20 };
// These two calls are equivalent:
let a1 = r.area();
let a2 = Rectangle::area(&r);
// For a mutable method:
r.double_size();
Rectangle::double_size(&mut r);
The dot notation is easier to read and write, but knowing the desugaring helps when you need to pass methods as function pointers or when reasoning about ownership. It also explains why associated functions that do not take self are called with :: — there is no instance to place before the dot.
Common Mistakes and Misconceptions
Using self when &self would suffice. A method that takes ownership of self destroys the original binding. Unless you are deliberately transforming the instance into something else, prefer &self or &mut self. Accidentally consuming a struct is a frequent source of compile errors.
Forgetting that self implies the struct type. Inside impl Rectangle, self is always Rectangle. You do not need to spell out Rectangle in the signature; Rust infers it.
Expecting a separate -> operator. Newcomers from C++ sometimes look for an arrow operator to call methods on references or smart pointers. Rust’s automatic referencing handles that uniformly, so the dot works everywhere.
Assuming methods always need &self. While most methods borrow the instance, the choice of self, &self, or &mut self is deliberate. Each communicates the method’s relationship to the instance and influences the borrow checker’s checks.
Summary
Method syntax is Rust’s way of attaching behaviour to a type without a class system. By writing an impl block, you collect all the operations that make sense for a struct in one place. The self parameter explicitly states whether a method reads, mutates, or consumes the instance, keeping ownership rules visible in the signature. Associated functions give you constructor-like patterns without special syntax, and Rust’s automatic referencing removes the need for explicit pointer dereferencing when calling methods.
What you’ve learned here about methods on structs applies equally to enums and trait objects. The impl block is the gateway to building rich types that carry both data and the logic that operates on it.