Methods and impl blocks
Intermediate · Language basics
What & why
A method is a function that belongs to a type. Instead of area(rectangle), you write rectangle.area() — the data and the behavior that acts on it live together. You’ve been calling methods all along: text.len(), numbers.push(5), String::from("hi"). This lesson shows you how to write your own, using impl blocks, and how to pick the right self so ownership stays happy.
The idea, slowly
The impl block: attaching behavior to a type
You define a struct (the data), then write an impl block (“implementation”) to hang methods on it:
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect = Rectangle { width: 3, height: 4 };
println!("area is {}", rect.area()); // 12
}
Read impl Rectangle { ... } as “here are the things a Rectangle can do.” Inside, area looks almost like a normal function, except its first parameter is the special word self. When you call rect.area(), Rust passes rect in as self automatically. So inside the method, self is the rectangle you called it on, and self.width reaches its field.
The dot before area is the giveaway: rect.area() is method-call syntax, and self is the value on the left of the dot.
The three receivers: &self, &mut self, self
The first parameter of a method — called the receiver — comes in three flavors, and choosing the right one is really a question about ownership (remember the Ownership lesson):
&self— “let me look at the data.” Borrows the value immutably. Use this when the method only reads. Most methods are&self.&mut self— “let me change the data.” Borrows mutably. Use this when the method needs to modify a field.self— “give me the value; I’m taking it.” Takes ownership, consuming the value. Use this rarely — only when the method transforms the value into something else and the original shouldn’t be used afterward.
Here they are side by side:
struct Counter {
count: u32,
}
impl Counter {
fn get(&self) -> u32 { // reads only → &self
self.count
}
fn increment(&mut self) { // changes a field → &mut self
self.count += 1;
}
fn into_total(self) -> u32 { // consumes self → self
self.count
}
}
fn main() {
let mut c = Counter { count: 0 };
c.increment();
c.increment();
println!("count is {}", c.get()); // 2
let total = c.into_total(); // c is consumed here
println!("final total {total}");
// c can no longer be used — it was moved into into_total
}
Think of &self as borrowing your friend’s book to read it, &mut self as borrowing it to scribble a note, and self as them giving you the book for keeps. The compiler enforces this: to call increment, the variable must be mut, because you’re borrowing it mutably.
Associated functions: methods without a receiver
Some functions belong to a type but don’t act on an existing instance — most commonly, functions that create one. These leave off self entirely and are called associated functions. You call them with :: instead of a dot:
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// no self → associated function, a constructor
fn new(width: u32, height: u32) -> Rectangle {
Rectangle { width, height }
}
fn square(size: u32) -> Rectangle {
Rectangle { width: size, height: size }
}
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect = Rectangle::new(3, 4); // :: because there's no instance yet
let sq = Rectangle::square(5);
println!("{} and {}", rect.area(), sq.area());
}
You already know one of these: String::from("hi"). from is an associated function on String — no existing string to act on, so it uses :: and returns a fresh one. new is the conventional name for a constructor, but it’s just a regular associated function, nothing magic.
The pattern to remember: Type::function() when there’s no instance yet (creating one); value.method() when you already have one.
Methods keep code tidy
Why put behavior in an impl instead of loose functions? Because the logic that belongs to a type lives with that type. Anyone reading Rectangle sees everything it can do in one place, and calling rect.area() reads better than area(&rect). It’s the same reason you bundle data into a struct — grouping what belongs together.
Common mistakes
- Using
&selfwhen you need to mutate. If a method changes a field, it must take&mut self. With&selfyou’ll get “cannot assign toself.x, which is behind a&reference.” Switch the receiver to&mut self. - Calling a
&mut selfmethod on a non-mutvalue.let c = Counter { ... }; c.increment();fails becausecisn’tmut. The variable must belet mut cto allow the mutable borrow. - Mixing up
.and::. Associated functions (noself) are called with:::Rectangle::new(...). Methods (withself) are called with a dot:rect.area(). Using the wrong one is a common early error. - Accidentally consuming with
self. A method that takesself(no&) moves the value; you can’t use the variable afterward. If you only meant to read, use&selfso the caller keeps ownership. - Forgetting
selfinside the method. Fields areself.width, not justwidth. Withoutself., Rust looks for a local variable namedwidthand won’t find one.
More examples
Reading a thermostat’s setting
A smart-home app needs to display the temperature without ever letting other code accidentally change it — a perfect job for &self.
struct Thermostat {
celsius: f64,
}
impl Thermostat {
fn fahrenheit(&self) -> f64 {
self.celsius * 9.0 / 5.0 + 32.0
}
}
fn main() {
let t = Thermostat { celsius: 22.0 };
println!("{:.1}F", t.fahrenheit());
}
Growing a shopping cart
An online store adds items to a cart every time the shopper clicks “add to cart” — that’s a change, so it needs &mut self.
struct Cart {
items: Vec<String>,
}
impl Cart {
fn add_item(&mut self, item: &str) {
self.items.push(item.to_string());
}
}
fn main() {
let mut cart = Cart { items: Vec::new() };
cart.add_item("keyboard");
cart.add_item("mouse");
println!("{:?}", cart.items);
}
Unwrapping a sealed envelope
Once you open a sealed envelope you can’t reseal it — some methods should consume their value and hand back what’s inside, never to be used again.
struct Envelope {
letter: String,
}
impl Envelope {
fn open(self) -> String {
self.letter
}
}
fn main() {
let envelope = Envelope { letter: String::from("You got the job!") };
let letter = envelope.open();
println!("{letter}");
// envelope can't be used anymore — it was consumed by open()
}
Building a user profile from parts
A constructor gathers scattered inputs — a name, an age — into one valid struct, so callers never have to build a UserProfile field-by-field.
struct UserProfile {
name: String,
age: u32,
}
impl UserProfile {
fn new(name: &str, age: u32) -> UserProfile {
UserProfile { name: name.to_string(), age }
}
}
fn main() {
let user = UserProfile::new("Priya", 29);
println!("{} is {}", user.name, user.age);
}
Configuring a server before it starts
Chaining self-consuming methods that each return Self lets you configure an object step by step, like ServerConfig::new().with_port(8080).
struct ServerConfig {
port: u16,
debug: bool,
}
impl ServerConfig {
fn new() -> ServerConfig {
ServerConfig { port: 80, debug: false }
}
fn with_port(mut self, port: u16) -> ServerConfig {
self.port = port;
self
}
fn with_debug(mut self, debug: bool) -> ServerConfig {
self.debug = debug;
self
}
}
fn main() {
let config = ServerConfig::new().with_port(8080).with_debug(true);
println!("port={} debug={}", config.port, config.debug);
}
Your turn
This program defines a BankAccount with a deposit method, but it won’t compile. The deposit method can’t change the balance, and the account it’s called on isn’t declared right. Fix both. Press ▶ Run.
struct BankAccount {
balance: u32,
}
impl BankAccount {
fn deposit(&self, amount: u32) {
self.balance += amount;
}
}
fn main() {
let account = BankAccount { balance: 100 };
account.deposit(50);
println!("balance is {}", account.balance);
}
Show solution
deposit changes a field, so it needs &mut self. And to call a &mut self method, account must be declared mut.
struct BankAccount {
balance: u32,
}
impl BankAccount {
fn deposit(&mut self, amount: u32) {
self.balance += amount;
}
}
fn main() {
let mut account = BankAccount { balance: 100 };
account.deposit(50);
println!("balance is {}", account.balance); // 150
}
The &mut self lets the method modify balance, and let mut account allows the mutable borrow.
Quick check
Remember this
- Methods live in an
impl Type { ... }block and take a receiver as their first parameter. &selfborrows to read (most methods),&mut selfborrows to change,selfconsumes the value (rare).- Calling a
&mut selfmethod requires the variable to bemut. - Associated functions have no
self(often constructors likenew) and are called withType::function(). - Method calls use a dot (
value.method()); associated functions use::(Type::func()).String::fromis a familiar example.
Go deeper
- Rust Book - Methods — Receiver forms and impl blocks.
Next: