Skip to content
Learn Rust
Theme

Methods

Data · Lesson 21

Methods go in an impl block. The first parameter says how the method uses the struct: read it, change it, or take it.

Coming from GoAn impl block holds the methods, and &mut self is your pointer receiver. A Go value receiver gets a copy; in Rust, plain self takes the value itself, and the caller can't use it again unless the type is Copy. &self is a pointer receiver that can only read, which Go can't say. Counter::new() is a naming habit, like NewCounter().

Coming from KotlinMethods go in a separate impl block, and there is no constructor: new is an ordinary function, like a factory in a companion object. A method that changes the struct takes &mut self, and a variable that owns the struct must be let mut to call it, where Kotlin lets a val call anything.

Coming from Pythonself is written out like Python's, but its form says what the method does: &self reads, &mut self changes, plain self uses it up. There is no __init__: Counter::new() is just a function that returns a Counter.

Coming from TypeScriptthis becomes an explicit self parameter, and its form says what the method does: &self reads, &mut self changes. There is no class or new: Counter::new() is a plain function, and a variable that owns the object must be let mut to call a &mut self method.

Coming from JavaMethods live in a separate impl block, and this is written out as the first parameter, with a mode: &self reads, &mut self changes, self takes the value. There are no constructors, so Counter::new() is a plain function, and two methods of one type can't share a name. A variable that owns the object must be let mut to call a &mut self method.

Coming from C#An impl block holds the methods, and fn new() with no self is a static factory, since Rust has no constructors. The receiver says whether a method changes the struct (&mut self) or only reads it (&self), which C# classes never spell out.

Coming from CThis is your counter_bump(struct Counter *c) pattern with the call written c.bump(): &self is like const struct Counter * and &mut self like struct Counter *. Where gcc only warns about passing a const object to a non-const pointer, Rust refuses to build.

Coming from C++&self is a const member function and &mut self is a non-const one, but the default flips: values are immutable unless declared let mut. There are no constructors; Counter::new() is an ordinary associated function that returns a value.

The quick comparison

Rustsrc/main.rsRun ↗
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter { // no self: called as Counter::new()
Counter { count: 0 }
}
fn get(&self) -> u32 { // &self: only reads
self.count
}
fn bump(&mut self) { // &mut self: changes the struct
self.count += 1;
}
}
fn main() {
let mut c = Counter::new();
c.bump();
c.bump();
println!("{}", c.get());
}
$ cargo run
2
package main
import "fmt"
type Counter struct {
count int
}
func NewCounter() *Counter {
return &Counter{}
}
func (c *Counter) Get() int {
return c.count
}
func (c *Counter) Bump() {
c.count++
}
func main() {
c := NewCounter()
c.Bump()
c.Bump()
fmt.Println(c.Get())
}
$ go run main.go
2
class Counter {
private var count = 0
fun get(): Int = count
fun bump() {
count++
}
}
fun main() {
val c = Counter()
c.bump()
c.bump()
println(c.get())
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
2
class Counter:
def __init__(self) -> None: # called as Counter()
self.count = 0
def get(self) -> int:
return self.count
def bump(self) -> None: # any method can change self
self.count += 1
c = Counter()
c.bump()
c.bump()
print(c.get())
$ python3 main.py
2
class Counter {
private count = 0;
get(): number { // only reads
return this.count;
}
bump(): void { // changes the object
this.count += 1;
}
}
const c = new Counter();
c.bump();
c.bump();
console.log(c.get());
$ tsc --strict --module nodenext --types node main.ts && node main.js
2
public class Main {
static class Counter {
private int count = 0;
int get() {
return count;
}
void bump() {
count++;
}
}
public static void main(String[] args) {
var c = new Counter();
c.bump();
c.bump();
System.out.println(c.get());
}
}
$ javac Main.java && java Main
2
var c = new Counter();
c.Bump();
c.Bump();
Console.WriteLine(c.Get());
class Counter
{
private uint count; // starts at 0
public uint Get() => count;
public void Bump() => count++;
}
$ dotnet run
2
#include <stdio.h>
struct Counter {
unsigned count;
};
// C has no methods: plain functions that take the struct by pointer
struct Counter counter_new(void) {
return (struct Counter){.count = 0};
}
unsigned counter_get(const struct Counter *c) { // const: only reads
return c->count;
}
void counter_bump(struct Counter *c) { // no const: changes it
c->count += 1;
}
int main(void) {
struct Counter c = counter_new();
counter_bump(&c);
counter_bump(&c);
printf("%u\n", counter_get(&c));
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main
2
#include <iostream>
class Counter {
public:
unsigned get() const { return count_; } // const: only reads
void bump() { count_ += 1; } // not const: changes the object
private:
unsigned count_ = 0; // a default member initializer: starts at 0
};
int main() {
Counter c;
c.bump();
c.bump();
std::cout << c.get() << "\n";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main
2

What's different

  • &self lets the method read the struct without copying it. &mut self lets the method change it. self (no &) means the method takes the struct; after calling it the caller can't use it (unless the type is Copy), and the method may hand it back.
  • new is not a keyword. It is just the usual name for a constructor function.
  • You call methods with a dot either way. Rust adds the & or &mut for you.
  • Two methods written in a type's own impl blocks can't share a name, so you can't overload by parameter list. Constructors get different names instead: new, with_capacity, from_parts. (Traits, covered later, can give one name several meanings, as From does for from.)
Gotcha

Calling a changing method on a non-mut variable

Looks fine: in Go, Kotlin, Python, TypeScript, Java, C# and C it runs fine. Rust won't compile it.

Works in Go. In Rust it's just a rule.Works in Kotlin. In Rust it's just a rule.Works in Python. In Rust it's just a rule.Works in TypeScript. In Rust it's just a rule.Works in Java. In Rust it's just a rule.Works in C#. In Rust it's just a rule.Works in C. In Rust it's just a rule.C++ refuses this too. Same idea, different words.
Rustsrc/main.rs✗ won't compile
struct Counter {
count: u32,
}
impl Counter {
fn bump(&mut self) {
self.count += 1;
}
}
fn main() {
let c = Counter { count: 0 };
c.bump();
println!("{}", c.count);
}
package main
import "fmt"
type Counter struct{ count int }
func (c *Counter) Bump() { c.count++ }
func main() {
c := Counter{}
c.Bump()
fmt.Println(c.count)
}
$ go run main.go✓ runs
1
class Counter {
var count = 0
fun bump() { count++ }
}
fun main() {
val c = Counter()
c.bump()
println(c.count)
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar✓ runs
1
class Counter:
def __init__(self) -> None:
self.count = 0
def bump(self) -> None:
self.count += 1
c = Counter()
c.bump()
print(c.count)
$ python3 main.py✓ runs
1
class Counter {
count = 0;
bump(): void {
this.count += 1;
}
}
const c = new Counter();
c.bump();
console.log(c.count);
$ tsc --strict --module nodenext --types node main.ts && node main.js✓ runs
1
public class Main {
static class Counter {
int count = 0;
void bump() {
count++;
}
}
public static void main(String[] args) {
final var c = new Counter();
c.bump();
System.out.println(c.count);
}
}
$ javac Main.java && java Main✓ runs
1
var c = new Counter();
c.Bump();
Console.WriteLine(c.Count);
class Counter
{
public uint Count { get; private set; }
public void Bump() => Count++;
}
$ dotnet run✓ runs
1
#include <stdio.h>
struct Counter {
unsigned count;
};
void counter_bump(struct Counter *c) {
c->count += 1;
}
int main(void) {
const struct Counter c = {0};
counter_bump(&c);
printf("%u\n", c.count);
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main✓ runs
main.c: In function ‘main’:
main.c:13:18: warning: passing argument 1 of ‘counter_bump’ discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
   13 |     counter_bump(&c);
      |                  ^~
main.c:7:35: note: expected ‘struct Counter *’ but argument is of type ‘const struct Counter *’
    7 | void counter_bump(struct Counter *c) {
      |                   ~~~~~~~~~~~~~~~~^
1
C++✗ refuses too

In C++ you have to write const to get this error; in Rust you get it by writing nothing. bump is not a const method, so it can't be called on a const Counter. (-fpermissive in the message is the switch that would turn this error into a warning; without it, it is an error.)

#include <iostream>
struct Counter {
unsigned count = 0;
void bump() { count += 1; }
};
int main() {
const Counter c;
c.bump();
std::cout << c.count << "\n";
}
main.cpp: In function ‘int main()’:
main.cpp:10:11: error: passing ‘const Counter’ as ‘this’ argument discards qualifiers [-fpermissive]
   10 |     c.bump();
      |     ~~~~~~^~
main.cpp:5:10: note:   in call to ‘void Counter::bump()’
    5 |     void bump() { count += 1; }
      |          ^~~~
What does the compiler say? Guess first, then open.
$ cargo buildWhat is E0596?
error[E0596]: cannot borrow `c` as mutable, as it is not declared as mutable
  --> src/main.rs:13:5
   |
13 |     c.bump();
   |     ^ cannot borrow as mutable
   |
help: consider changing this to be mutable
   |
12 |     let mut c = Counter { count: 0 };
   |         +++
What happens in Go

Go runs this and prints 1. Go takes &c for you behind the scenes when c is a variable, so it can call a pointer method.

What happens in Kotlin

Kotlin runs this and prints 1. val only stops you from reassigning c, not from changing what's inside it.

What happens in Python

Python prints 1. There is no mut: any name can call any method, and the object changes. An ordinary class has no way to mark a method as "changes the object" or a variable as "never changes" (a frozen dataclass opts in per class).

What happens in TypeScript

Prints 1. const only stops you from pointing c at a different object; the object itself can still change. (readonly fields opt in, one by one.)

What happens in Java

This prints 1. In Java, final only stops c from pointing at another object; the object itself can still change.

What happens in C#

A method can change a class's (non-readonly) fields in C#, so this prints 1. There is no marker at the call site that says this call changes c.

What happens in C

gcc only warns that &c drops the const, then lets counter_bump change it and prints 1. Changing a const object is undefined behaviour, so this is luck.

What happens in C++

In C++ you have to write const to get this error; in Rust you get it by writing nothing. bump is not a const method, so it can't be called on a const Counter. (-fpermissive in the message is the switch that would turn this error into a warning; without it, it is an error.)

What Rust sees

bump says "I change the counter" (&mut self). But c was declared with plain let, which means "this never changes".

Why Rust says no

In Rust, let without mut covers the whole value, including everything inside it. That is stronger than Java's final, Kotlin's val or a JavaScript const, which only stop the variable from pointing somewhere else. It is what lets you trust that a non-mut value really stays the same (a few types, such as Cell and Mutex, change through & on purpose; see Shared state).

The fix

Compiles and runs.

Rust, fixedsrc/main.rsRun ↗
struct Counter {
count: u32,
}
impl Counter {
fn bump(&mut self) {
self.count += 1;
}
}
fn main() {
let mut c = Counter { count: 0 };
c.bump();
println!("{}", c.count);
}
$ cargo run
1

The toolkit

A struct holds data. A method is a function that belongs to a type, so you can write acct.deposit(5) instead of deposit(&mut acct, 5). This lesson covers writing methods, what the first parameter means, and what Rust uses in place of constructors, overloading and inheritance. Outputs come from Linux on 64-bit ARM.

A method is a function with a self parameter

Methods go inside an impl block (“implementation”), named after the type they belong to:

Rustsrc/main.rsRun ↗
struct Account {
owner: String,
balance: u32,
}
impl Account {
fn new(owner: &str) -> Self {
Self {
owner: owner.to_string(),
balance: 0,
}
}
fn balance(&self) -> u32 {
self.balance
}
fn deposit(&mut self, amount: u32) {
self.balance += amount;
}
fn close(self) -> String {
format!("{} closed with {}", self.owner, self.balance)
}
}
fn main() {
let mut acct = Account::new("ana");
acct.deposit(50);
acct.deposit(25);
println!("balance: {}", acct.balance());
let note = acct.close();
println!("note: {note}");
}
$ cargo run
balance: 75
note: ana closed with 75
  • impl Account { ... } says “the functions in here belong to Account”.
  • Self inside the block is another name for Account, so the body of the block survives a rename of the type.
  • A function whose first parameter is self is a method. self is the value you called it on, so self.balance is the balance field of acct.
  • A function with no self, like new, is an associated function. You call it with :: on the type: Account::new("ana"). More on that below.

A method and a field can share a name: acct.balance is the field and acct.balance() is the method. This is the usual way to give read access to a field, with no get_ prefix (API guidelines).

What the first parameter means

deposit takes &mut self, balance takes &self and close takes self. They are not three styles. The value you call a method on is the receiver, and the first parameter says what the method does with it, in the same words as the ownership, borrowing and mutable borrows lessons:

Write It is short for The method may The caller’s variable
&self self: &Self read the value a plain let is enough; still usable afterwards
&mut self self: &mut Self read and change the value a variable that owns the value must be let mut; one that already holds a &mut Account needs nothing; still usable afterwards
self self: Self take the value: drop it, store it, or return it a plain let is enough; the value is moved, so the variable can’t be used again (a Copy type such as a number is copied instead)
mut self self: Self, and the method may change it change the value it now owns, and hand it back a plain let is enough; moved

mut self is the row to read twice. It does not make the caller’s variable changeable: the method owns the value it was given, and mut lets it change that value before returning it. The builder below uses it.

Rule of thumb: start with &self, use &mut self when the method changes the value, and self when it finishes with the value (turns it into something else, or hands its parts on). A method that turns a value into another one and uses it up is conventionally named into_... (API guidelines).

The compiler checks the method against its own signature. A &self method can’t change anything:

Rustsrc/main.rs✗ won't compile
struct Account {
balance: u32,
}
impl Account {
fn reset(&self) {
self.balance = 0;
}
}
fn main() {
let acct = Account { balance: 5 };
acct.reset();
println!("balance: {}", acct.balance);
}
$ cargo buildWhat is E0594?
error[E0594]: cannot assign to `self.balance`, which is behind a `&` reference
 --> src/main.rs:7:9
  |
7 |         self.balance = 0;
  |         ^^^^^^^^^^^^^^^^ `self` is a `&` reference, so it cannot be written to
  |
help: consider changing this to be a mutable reference
  |
6 |     fn reset(&mut self) {
  |               +++

Calling a &mut self method on a plain let is the other direction, and it is the gotcha at the top of this page (E0596). The third direction is a method that takes self, followed by a use of the variable:

Rustsrc/main.rs✗ won't compile
struct Account {
owner: String,
balance: u32,
}
impl Account {
fn close(self) -> String {
format!("{} closed with {}", self.owner, self.balance)
}
}
fn main() {
let acct = Account {
owner: String::from("ana"),
balance: 75,
};
let note = acct.close();
println!("note: {note}");
println!("balance: {}", acct.balance);
}
$ cargo buildWhat is E0382?
error[E0382]: borrow of moved value: `acct`
  --> src/main.rs:19:29
   |
13 |     let acct = Account {
   |         ---- move occurs because `acct` has type `Account`, which does not implement the `Copy` trait
...
17 |     let note = acct.close();
   |                     ------- `acct` moved due to this method call
18 |     println!("note: {note}");
19 |     println!("balance: {}", acct.balance);
   |                             ^^^^^^^^^^^^ value borrowed here after move
   |
note: `Account::close` takes ownership of the receiver `self`, which moves `acct`
  --> src/main.rs:7:14
   |
 7 |     fn close(self) -> String {
   |              ^^^^

The note at the end names the cause: Account::close takes ownership of the receiver self. That is a move, the same as passing acct to a function. The signature tells every caller what happens, with no documentation to read. So a type can say “after this call you are finished with me”. Using something already closed is a run-time error in many languages, and an E0382 here.

Functions without self: new, Self and constants

A function in an impl block with no self belongs to the type but not to a value. These associated functions are where constructors go. Methods and associated functions written in the type’s own impl blocks are called inherent (as opposed to the ones a trait supplies):

Rustsrc/main.rsRun ↗
struct Account {
owner: String,
balance: u32,
}
impl Account {
const MAX_BALANCE: u32 = 1_000;
fn new(owner: &str) -> Self {
Self::with_balance(owner, 0)
}
fn with_balance(owner: &str, balance: u32) -> Self {
Self {
owner: owner.to_string(),
balance: balance.min(Self::MAX_BALANCE),
}
}
}
// A second `impl` block for the same type is allowed.
impl Account {
fn is_full(&self) -> bool {
self.balance == Self::MAX_BALANCE
}
}
fn main() {
let a = Account::new("ana");
let b = Account::with_balance("bo", 5_000);
println!("a: {} has {}, full: {}", a.owner, a.balance, a.is_full());
println!("b: {} has {}, full: {}", b.owner, b.balance, b.is_full());
println!("limit: {}", Account::MAX_BALANCE);
}
$ cargo run
a: ana has 0, full: false
b: bo has 1000, full: true
limit: 1000
  • new is not a keyword. It is a name people agree on for “the main way to make one”. The standard library also has String::from, Vec::with_capacity and File::open. A struct has no hidden constructor: anywhere its fields are visible, you can build it with Account { owner, balance } (privacy, below).
  • Self::with_balance(owner, 0) calls another associated function of the same type. Self { ... } builds a value.
  • const MAX_BALANCE: u32 = 1_000; inside impl is an associated constant: one value for the whole type, used as Account::MAX_BALANCE outside and Self::MAX_BALANCE inside.
  • A type can have several impl blocks; together they are one set of methods. A trait’s methods get a block of their own (Traits).

Two inherent methods of one type can’t share a name, so you can’t overload by parameter list. Constructors that take different arguments get different names, as new and with_balance do:

Rustsrc/main.rs✗ won't compile
struct Account {
owner: String,
balance: u32,
}
impl Account {
fn new(owner: &str) -> Self {
Self {
owner: owner.to_string(),
balance: 0,
}
}
fn new(owner: &str, balance: u32) -> Self {
Self {
owner: owner.to_string(),
balance,
}
}
}
fn main() {
let a = Account::new("ana");
println!("{} has {}", a.owner, a.balance);
}
$ cargo buildWhat is E0592?
error[E0592]: duplicate definitions with name `new`
  --> src/main.rs:14:5
   |
 7 |     fn new(owner: &str) -> Self {
   |     --------------------------- other definition for `new`
...
14 |     fn new(owner: &str, balance: u32) -> Self {
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ duplicate definitions for `new`

The exception is a trait: the standard library’s From can be implemented twice for one type, impl From<u8> for Y and impl From<&str> for Y, giving Y::from(7u8) and Y::from("abc"). That is a trait feature (Traits), not overloading of your own methods.

The dot does the borrowing for you

You never write (&mut acct).deposit(5). At a ., the compiler works out whether the method needs the value, & or &mut, and adds the borrow for you. It also looks through references, so a method works the same on a value and on a reference to it:

Rustsrc/main.rsRun ↗
struct Account {
balance: u32,
}
impl Account {
fn balance(&self) -> u32 {
self.balance
}
fn deposit(&mut self, amount: u32) {
self.balance += amount;
}
}
fn top_up(acct: &mut Account) {
acct.deposit(10); // `acct` is already a `&mut Account`; no `(*acct)` needed
}
fn main() {
let mut acct = Account { balance: 0 };
acct.deposit(5); // the compiler passes `&mut acct`
top_up(&mut acct);
println!("value: {}", acct.balance());
let r = &acct;
let rr = &r;
println!("&Account: {}", r.balance());
println!("&&Account: {}", rr.balance());
let mut accounts = vec![Account { balance: 1 }, Account { balance: 2 }];
for a in &mut accounts {
a.deposit(100); // `a` is a `&mut Account`
}
println!("accounts[0]: {}", accounts[0].balance());
println!("accounts[1]: {}", accounts[1].balance());
}
$ cargo run
value:       15
&Account:    15
&&Account:   15
accounts[0]: 101
accounts[1]: 102

Inside top_up, acct is already a &mut Account and acct.deposit(10) works with no (*acct) in front. The same goes for a in the loop and for a &&Account. A method that takes self is the exception:

Rustsrc/main.rs✗ won't compile
struct Account {
owner: String,
}
impl Account {
fn close(self) -> String {
format!("{} closed", self.owner)
}
}
fn close_it(acct: &Account) -> String {
acct.close()
}
fn main() {
let acct = Account {
owner: String::from("ana"),
};
println!("{}", close_it(&acct));
}
$ cargo buildWhat is E0507?
error[E0507]: cannot move out of `*acct` which is behind a shared reference
  --> src/main.rs:12:5
   |
12 |     acct.close()
   |     ^^^^ ------- `*acct` moved due to this method call
   |     |
   |     move occurs because `*acct` has type `Account`, which does not implement the `Copy` trait
   |
note: `Account::close` takes ownership of the receiver `self`, which moves `*acct`
  --> src/main.rs:6:14
   |
 6 |     fn close(self) -> String {
   |              ^^^^
note: if `Account` implemented `Clone`, you could clone the value
  --> src/main.rs:1:1
   |
 1 | struct Account {
   | ^^^^^^^^^^^^^^ consider implementing `Clone` for this type
...
12 |     acct.close()
   |     ---- you could clone this value

E0507: close_it only borrowed the account, so it cannot hand it to close, which wants to own it. The second note suggests Clone, a way to make a copy; usually the better fix is the signature: take Account by value in close_it, or make close take &self.

Method syntax is shorthand. Type::method(receiver, arguments) works for any method defined on Type, and it is the form to use when you need a method as a plain function:

Rustsrc/main.rsRun ↗
struct Account {
balance: u32,
}
impl Account {
fn balance(&self) -> u32 {
self.balance
}
fn deposit(&mut self, amount: u32) {
self.balance += amount;
}
}
fn main() {
let mut acct = Account { balance: 0 };
acct.deposit(5);
Account::deposit(&mut acct, 5); // the same call, written out
println!("method syntax: {}", acct.balance());
println!("function syntax: {}", Account::balance(&acct));
// A method is a function whose first parameter is the receiver, so it can be
// stored in a variable and passed around.
let read: fn(&Account) -> u32 = Account::balance;
println!("through a variable: {}", read(&acct));
let accounts = vec![Account { balance: 3 }, Account { balance: 4 }];
let total: u32 = accounts.iter().map(Account::balance).sum();
println!("total: {total}");
}
$ cargo run
method syntax:   10
function syntax: 10
through a variable: 10
total: 7

read holds a function (fn(&Account) -> u32 is the type of a function from &Account to u32). accounts.iter().map(Account::balance).sum() calls balance on every element of a list and adds the results (iterators covers iter, map and sum). The path must name the type that defines the method: iter belongs to slices, so Vec::iter(&v) is an error (E0599) and <[i32]>::iter(&v) works. The dot form is not a value: let f = acct.deposit; is an error (E0615), where the path form Account::deposit is fine.

Chaining and builders

When a method returns something that has more methods, calls can be chained: Request::new(url).retries(3).header("X-Id: 7"). Rust has no default or named arguments (see Constructors, below), so a type with many optional settings gives each setting its own method. That is the builder pattern, and a chain is how you use it.

A builder method has two possible shapes. This one takes mut self and returns Self, so every call moves the builder in and hands it back:

Rustsrc/main.rsRun ↗
struct Request {
url: String,
retries: u32,
timeout_secs: u32,
headers: Vec<String>,
}
impl Request {
fn new(url: &str) -> Self {
Self {
url: url.to_string(),
retries: 0,
timeout_secs: 30,
headers: Vec::new(),
}
}
fn retries(mut self, n: u32) -> Self {
self.retries = n;
self
}
fn timeout(mut self, secs: u32) -> Self {
self.timeout_secs = secs;
self
}
fn header(mut self, line: &str) -> Self {
self.headers.push(line.to_string());
self
}
}
fn main() {
let req = Request::new("https://example.com")
.retries(3)
.header("Accept: text/plain")
.header("X-Id: 7")
.timeout(5);
println!("url: {}", req.url);
println!("retries: {}", req.retries);
println!("timeout: {}", req.timeout_secs);
println!("headers: {:?}", req.headers);
// Adding a header only sometimes means putting the value back:
let verbose = true;
let mut req = Request::new("https://example.com");
if verbose {
req = req.header("X-Debug: 1");
}
println!("headers: {:?}", req.headers);
}
$ cargo run
url: https://example.com
retries: 3
timeout: 5
headers: ["Accept: text/plain", "X-Id: 7"]
headers: ["X-Debug: 1"]

It reads like return this, but the old name is dead afterwards, which is why a setting made only sometimes needs req = req.header(...). No String or Vec contents are copied. Here Request is its own builder; a separate builder type is worth it when the finished value must be checked or has a different type.

The other shape borrows: &mut self in, &mut Self out. The builder here is a separate type that makes a Request at the end:

Rustsrc/main.rsRun ↗
struct Request {
url: String,
retries: u32,
headers: Vec<String>,
}
struct RequestBuilder {
url: String,
retries: u32,
headers: Vec<String>,
}
impl RequestBuilder {
fn new(url: &str) -> Self {
Self {
url: url.to_string(),
retries: 0,
headers: Vec::new(),
}
}
fn retries(&mut self, n: u32) -> &mut Self {
self.retries = n;
self
}
fn header(&mut self, line: &str) -> &mut Self {
self.headers.push(line.to_string());
self
}
fn build(&self) -> Request {
Request {
url: self.url.clone(),
retries: self.retries,
headers: self.headers.clone(),
}
}
}
fn main() {
// One expression: the temporary builder lives until the end of the statement.
let one = RequestBuilder::new("https://example.com")
.retries(3)
.header("Accept: text/plain")
.build();
println!("one: {} {} {:?}", one.url, one.retries, one.headers);
// Step by step: the builder is a `let mut` variable, and a method call does
// not need its result put back.
let verbose = true;
let mut b = RequestBuilder::new("https://example.com");
b.retries(1);
if verbose {
b.header("X-Debug: 1");
}
let two = b.build();
println!("two: {} {} {:?}", two.url, two.retries, two.headers);
}
$ cargo run
one:  https://example.com 3 ["Accept: text/plain"]
two:  https://example.com 1 ["X-Debug: 1"]

Now b.header(...) changes b in place, so the conditional needs no re-assigning. The price is in build: it takes &self, so it clones the strings (clone makes a copy). Why not build(self)? A &mut Self chain only hands the next call a borrow, and a self method can’t be called through one:

Rustsrc/main.rs✗ won't compile
struct Builder {
name: String,
}
impl Builder {
fn new() -> Self {
Self {
name: String::new(),
}
}
fn name(&mut self, name: &str) -> &mut Self {
self.name = name.to_string();
self
}
fn build(self) -> String {
self.name
}
}
fn main() {
let text = Builder::new().name("ana").build();
println!("built: {text}");
}
$ cargo buildWhat is E0507?
error[E0507]: cannot move out of a mutable reference
  --> src/main.rs:23:16
   |
23 |     let text = Builder::new().name("ana").build();
   |                ^^^^^^^^^^^^^^^^^^^^^^^^^^ ------- value moved due to this method call
   |                |
   |                move occurs because value has type `Builder`, which does not implement the `Copy` trait
   |
note: `Builder::build` takes ownership of the receiver `self`, which moves value
  --> src/main.rs:17:14
   |
17 |     fn build(self) -> String {
   |              ^^^^
note: if `Builder` implemented `Clone`, you could clone the value
  --> src/main.rs:1:1
   |
 1 | struct Builder {
   | ^^^^^^^^^^^^^^ consider implementing `Clone` for this type
...
23 |     let text = Builder::new().name("ana").build();
   |                -------------------------- you could clone this value

Same E0507 as before. There is also a trap with temporaries: values built in the middle of an expression with no variable, dropped at the end of the statement:

Rustsrc/main.rs✗ won't compile
struct Builder {
retries: u32,
}
impl Builder {
fn new() -> Self {
Self { retries: 0 }
}
fn retries(&mut self, n: u32) -> &mut Self {
self.retries = n;
self
}
fn build(&self) -> u32 {
self.retries
}
}
fn main() {
let b = Builder::new().retries(3);
println!("built: {}", b.build());
}
$ cargo buildWhat is E0716?
error[E0716]: temporary value dropped while borrowed
  --> src/main.rs:21:13
   |
21 |     let b = Builder::new().retries(3);
   |             ^^^^^^^^^^^^^^           - temporary value is freed at the end of this statement
   |             |
   |             creates a temporary value which is freed while still in use
22 |     println!("built: {}", b.build());
   |                           - borrow later used here
   |
help: consider using a `let` binding to create a longer lived value
   |
21 ~     let mut binding = Builder::new();
22 ~     let b = binding.retries(3);

Builder::new() is a temporary. retries(3) returns a &mut into it, and the next line uses that after it is gone: E0716. The fix is to keep the builder in a let mut variable first. In one statement, as in builder-mut-ref.rs, the temporary lasts long enough.

mut self -> Self &mut self -> &mut Self
One expression, new().a().b()... works works
A final build(self) that moves the parts out works E0507
Setting an option only sometimes b = b.a() b.a();
let b = Builder::new().a(); works E0716

The API guidelines prefer the borrowing shape, and use the consuming one when the result needs the builder’s contents moved into it. The standard library uses both: Command::arg takes &mut self and returns &mut Command, while thread::Builder::name takes self and returns Builder.

Methods on tuple structs

A tuple struct has fields without names, self.0, self.1, and takes methods the same way:

Rustsrc/main.rsRun ↗
struct Score(u32);
impl Score {
fn new(points: u32) -> Self {
Self(points)
}
fn add(&mut self, points: u32) {
self.0 += points;
}
fn is_high(&self) -> bool {
self.0 >= 100
}
fn into_inner(self) -> u32 {
self.0
}
}
fn main() {
let mut s = Score::new(90);
s.add(15);
println!("high: {}", s.is_high());
println!("inner: {}", s.into_inner());
}
$ cargo run
high: true
inner: 105

Score(90) builds a value, and Self(points) does the same inside the block. Wrapping a number this way gives it its own methods and stops it being mixed up with other numbers.

Methods and privacy

Rust hides fields with a different boundary than Java or C#: the module. A module (mod bank { ... }) is a named block that groups code. Fields and methods are private by default: only the module they are defined in, and the modules nested inside it, can use them. pub makes one visible outside (Reference).

Rustsrc/main.rs✗ won't compile
mod bank {
pub struct Account {
balance: u32,
}
impl Account {
pub fn new() -> Self {
Account { balance: 0 }
}
pub fn deposit(&mut self, amount: u32) {
self.balance += amount;
}
pub fn balance(&self) -> u32 {
self.balance
}
}
}
fn main() {
let mut acct = bank::Account::new();
acct.deposit(5);
acct.balance = 1_000_000;
println!("balance: {}", acct.balance());
}
$ cargo buildWhat is E0616?
error[E0616]: field `balance` of struct `Account` is private
  --> src/main.rs:24:10
   |
24 |     acct.balance = 1_000_000;
   |          ^^^^^^^ private field

Code outside bank can call deposit and balance, but not touch balance. Methods are how a type keeps its own rules (a balance that only changes through deposit). Hide a field when a rule needs protecting; plain data can stay pub. Inside bank, any code can touch the fields: the boundary is the module, not the type.

Method or function?

Use a method when the code is about one value of the type and reads as value.action(), or when the type must protect private fields. Use a plain function when two values play equal parts (distance(a, b)), when there is no natural owner, or when the type is not yours. A method is a function with a receiver (explicit-call.rs), so the choice is about readability. An inherent impl block must be in the crate that defines the type (a crate is one library or program: everything compiled together; Reference), so you can’t add inherent methods to str or Vec. Adding methods to someone else’s type works through a trait (Traits).

Your language’s methods in Rust

Pick your language in “Coming from” at the top of the page to see how the code you write today maps to Rust.

Going deeper

What self is in memory

acct.deposit(5) does not copy the Account. With &mut self, the method receives the address of acct. With self, the method receives the value itself, and acct is moved. This program prints the facts for both (as_ptr() is the address of the text; the program prints whether two addresses are equal):

Rustsrc/main.rsRun ↗
use std::mem::size_of;
struct Account {
owner: String,
balance: u32,
}
impl Account {
fn itself(&self) -> &Account {
self
}
fn balance(&self) -> u32 {
self.balance
}
fn text_address(&self) -> *const u8 {
self.owner.as_ptr()
}
fn close(self) -> *const u8 {
self.owner.as_ptr()
}
}
fn main() {
let acct = Account {
owner: String::from("ana"),
balance: 75,
};
println!("size of Account: {} bytes", size_of::<Account>());
println!("size of String: {} bytes", size_of::<String>());
println!("size of u32: {} bytes", size_of::<u32>());
println!(
"owner: {} bytes of text, capacity {}",
acct.owner.len(),
acct.owner.capacity()
);
println!("balance: {}", acct.balance());
println!(
"&self is the caller's variable: {}",
std::ptr::eq(acct.itself(), &acct)
);
let before = acct.text_address();
let after = acct.close();
println!("same text buffer after close: {}", before == after);
}
$ cargo run
size of Account: 32 bytes
size of String:  24 bytes
size of u32:     4 bytes
owner: 3 bytes of text, capacity 3
balance: 75
&self is the caller's variable: true
same text buffer after close: true

An Account is a String (24 bytes) plus a u32 (4 bytes), rounded up to 32. The text ana is on the heap. With &self or &mut self:

acct.deposit(5) // inside deposit(&mut self, amount)
STACKHEAPacct: Accountowner ptrowner len 3owner cap 3balance 75self: &mut Accountaddress of accta borrow, not a copy"ana": 3 bytes of textana
self holds one address. std::ptr::eq(acct.itself(), &acct) printed true: the method sees the caller's variable, not a copy of it.

And self by value, acct.close():

acct.close() // inside close(self)
STACKHEAPacct: Account (moved)owner ptrowner len 3owner cap 3balance 75the compiler will not let you use itself: Accountowner ptrowner len 3owner cap 3balance 75the method owns the value now"ana": 3 bytes of textana
The 32 bytes of the struct move into close; the compiler may copy them or not. The text is not copied: its address before and after was the same (true above). When close ends, self is dropped and the text is freed. acct is not dropped a second time.

Methods are not stored in the value. Counter below has three methods and is still the size of its one u32 field:

Rustsrc/main.rsRun ↗
use std::mem::size_of;
struct Counter {
count: u32,
}
impl Counter {
fn get(&self) -> u32 {
self.count
}
fn bump(&mut self) {
self.count += 1;
}
fn into_value(self) -> u32 {
self.count
}
}
fn main() {
let mut c = Counter { count: 0 };
c.bump();
println!("count: {}", c.get());
println!("size of Counter: {} bytes", size_of::<Counter>());
println!("size of u32: {} bytes", size_of::<u32>());
println!("into_value: {}", c.into_value());
}
$ cargo run
count: 1
size of Counter: 4 bytes
size of u32:     4 bytes
into_value: 1

A call like c.bump() goes to a function the compiler picked while compiling, not to an entry in a table inside c. Choosing between types at run time (what a Go interface does) is a different feature, with its own lesson later.

How the compiler finds the method

For receiver.name(...), the compiler builds a list of candidate types: the receiver’s type T, then each type reached by dereferencing (looking through a reference, a Box, an Rc or another smart pointer, but not a raw pointer), and for an array, the slice it can be viewed as. After each candidate T it adds &T and &mut T. Going down the list, it looks for a method whose self has exactly that type. At each type the type’s own inherent methods come first, then trait methods. The first match wins, and two matches at the same step are an error: you then say which one with Type::method(&x). This is the rule in the Rust Reference. It is why rr.balance() worked on a &&Account in auto-ref.rs: dereferencing once gives &Account, exactly what balance wants.

No inheritance

Rust has no extends. A type that reuses another one holds it as a field:

Rustsrc/main.rsRun ↗
struct Account {
balance: u32,
}
impl Account {
fn balance(&self) -> u32 {
self.balance
}
fn deposit(&mut self, amount: u32) {
self.balance += amount;
}
}
// There is no `extends`. A bigger type holds the smaller one as a field.
struct Savings {
account: Account,
rate_percent: u32,
}
impl Savings {
fn deposit(&mut self, amount: u32) {
self.account.deposit(amount); // forwarded by hand
}
fn interest(&self) -> u32 {
self.account.balance() * self.rate_percent / 100
}
}
fn main() {
let mut s = Savings {
account: Account { balance: 0 },
rate_percent: 5,
};
s.deposit(200);
println!("balance: {}", s.account.balance());
println!("interest: {}", s.interest());
}
$ cargo run
balance:  200
interest: 10

Savings has an Account, and deposit is forwarded by hand. That is more typing than inheritance or Go’s embedding. In return, Savings offers exactly what you wrote and nothing is overridden behind your back. A &Savings is not accepted where a &Account is wanted; you pass &s.account. Code that must accept several types uses traits.

Constructors, overloading and default arguments

A constructor is a function, usually new; two inherent methods can’t share a name; a parameter can’t have a default. The answers are different names for different ways to build, a struct literal, and a builder for many settings (above). A builder is more code than a default argument, and that is a real cost of the design.

A struct literal is the closest thing to named arguments: Account { owner: ..., balance: ... }. It needs a value for every field unless you end it with ..Default::default() or ..other (Structs), and only code that can see the fields can write one.

Practice

Try it

Open each exercise in the Playground, replace the todo!(), and run it until it prints all checks passed.

Basket

Write four methods for Basket: new, count (reads), add (changes) and into_receipt (uses the basket up). add trims the text (item.trim() removes spaces at both ends; see Strings) and refuses a blank item. The receipt joins the items with , using a loop. An empty basket gives (empty).

Starterexercises/basket.rs
struct Basket {
items: Vec<String>,
}
impl Basket {
/// An empty basket.
fn new() -> Self {
todo!("build a Basket whose items field is Vec::new()")
}
/// How many items are in the basket. It only reads.
fn count(&self) -> usize {
todo!("self.items.len()")
}
/// Adds the item with spaces trimmed off both ends and returns true.
/// A blank item (empty, or only spaces) is not added: return false.
fn add(&mut self, item: &str) -> bool {
todo!("trim() the item; if it is empty return false, else push a String")
}
/// Uses the basket up and returns the items joined with ", ".
/// An empty basket gives "(empty)".
fn into_receipt(self) -> String {
todo!("loop over &self.items, push_str each, and put \", \" between them")
}
}
fn main() {
let mut basket = Basket::new();
assert_eq!(basket.count(), 0);
assert!(basket.add("café"));
assert!(basket.add(" tea "));
assert!(!basket.add(" "), "a blank item is not added");
assert!(!basket.add(""), "an empty item is not added");
assert_eq!(basket.count(), 2);
assert_eq!(basket.into_receipt(), "café, tea");
let empty = Basket::new();
assert_eq!(empty.into_receipt(), "(empty)");
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/basket.solution.rsRun ↗
struct Basket {
items: Vec<String>,
}
impl Basket {
/// An empty basket.
fn new() -> Self {
Self { items: Vec::new() }
}
/// How many items are in the basket. It only reads.
fn count(&self) -> usize {
self.items.len()
}
/// Adds the item with spaces trimmed off both ends and returns true.
/// A blank item (empty, or only spaces) is not added: return false.
fn add(&mut self, item: &str) -> bool {
let item = item.trim();
if item.is_empty() {
return false;
}
self.items.push(item.to_string());
true
}
/// Uses the basket up and returns the items joined with ", ".
/// An empty basket gives "(empty)".
fn into_receipt(self) -> String {
if self.items.is_empty() {
return String::from("(empty)");
}
let mut receipt = String::new();
for item in &self.items {
if !receipt.is_empty() {
receipt.push_str(", ");
}
receipt.push_str(item);
}
receipt
}
}
fn main() {
let mut basket = Basket::new();
assert_eq!(basket.count(), 0);
assert!(basket.add("café"));
assert!(basket.add(" tea "));
assert!(!basket.add(" "), "a blank item is not added");
assert!(!basket.add(""), "an empty item is not added");
assert_eq!(basket.count(), 2);
assert_eq!(basket.into_receipt(), "café, tea");
let empty = Basket::new();
assert_eq!(empty.into_receipt(), "(empty)");
println!("all checks passed");
}
$ cargo run
all checks passed

Greeting builder

Finish a builder whose setters take mut self and return Self, and whose build produces the greeting with a plain loop. Mind a blank name and zero repeats. The last check shows what you do when an option is set only sometimes.

Starterexercises/greeting-builder.rs
struct GreetingBuilder {
name: String,
excited: bool,
times: u32,
}
impl GreetingBuilder {
/// No name, not excited, said once.
fn new() -> Self {
todo!("name is String::new(), excited is false, times is 1")
}
/// Sets the name. Each setter takes `mut self` and gives `self` back.
fn name(mut self, name: &str) -> Self {
todo!("self.name = name.to_string(); self")
}
/// `true` ends the greeting with "!", `false` with ".".
fn excited(mut self, yes: bool) -> Self {
todo!("set the field, return self")
}
/// How many times the greeting is repeated.
fn times(mut self, n: u32) -> Self {
todo!("set the field, return self")
}
/// "Hello, NAME." repeated `times` times, separated by one space.
/// The name is trimmed; a blank name becomes "stranger".
/// Zero times gives an empty String.
fn build(self) -> String {
todo!("trim the name, make one sentence, push_str it `times` times with spaces between")
}
}
fn main() {
assert_eq!(GreetingBuilder::new().build(), "Hello, stranger.");
assert_eq!(
GreetingBuilder::new().name("Zoë").excited(true).build(),
"Hello, Zoë!"
);
assert_eq!(
GreetingBuilder::new().times(2).name(" Åsa ").build(),
"Hello, Åsa. Hello, Åsa."
);
assert_eq!(
GreetingBuilder::new().name(" ").build(),
"Hello, stranger.",
"a blank name falls back to stranger"
);
assert_eq!(GreetingBuilder::new().times(0).build(), "");
// Setting an option only sometimes means putting the builder back.
let loud = true;
let mut builder = GreetingBuilder::new().name("Li");
if loud {
builder = builder.excited(true);
}
assert_eq!(builder.build(), "Hello, Li!");
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/greeting-builder.solution.rsRun ↗
struct GreetingBuilder {
name: String,
excited: bool,
times: u32,
}
impl GreetingBuilder {
/// No name, not excited, said once.
fn new() -> Self {
Self {
name: String::new(),
excited: false,
times: 1,
}
}
/// Sets the name. Each setter takes `mut self` and gives `self` back.
fn name(mut self, name: &str) -> Self {
self.name = name.to_string();
self
}
/// `true` ends the greeting with "!", `false` with ".".
fn excited(mut self, yes: bool) -> Self {
self.excited = yes;
self
}
/// How many times the greeting is repeated.
fn times(mut self, n: u32) -> Self {
self.times = n;
self
}
/// "Hello, NAME." repeated `times` times, separated by one space.
/// The name is trimmed; a blank name becomes "stranger".
/// Zero times gives an empty String.
fn build(self) -> String {
let name = self.name.trim();
let name = if name.is_empty() { "stranger" } else { name };
let end = if self.excited { "!" } else { "." };
let sentence = format!("Hello, {name}{end}");
let mut out = String::new();
for i in 0..self.times {
if i > 0 {
out.push(' ');
}
out.push_str(&sentence);
}
out
}
}
fn main() {
assert_eq!(GreetingBuilder::new().build(), "Hello, stranger.");
assert_eq!(
GreetingBuilder::new().name("Zoë").excited(true).build(),
"Hello, Zoë!"
);
assert_eq!(
GreetingBuilder::new().times(2).name(" Åsa ").build(),
"Hello, Åsa. Hello, Åsa."
);
assert_eq!(
GreetingBuilder::new().name(" ").build(),
"Hello, stranger.",
"a blank name falls back to stranger"
);
assert_eq!(GreetingBuilder::new().times(0).build(), "");
// Setting an option only sometimes means putting the builder back.
let loud = true;
let mut builder = GreetingBuilder::new().name("Li");
if loud {
builder = builder.excited(true);
}
assert_eq!(builder.build(), "Hello, Li!");
println!("all checks passed");
}
$ cargo run
all checks passed

Celsius

A tuple struct with an associated constant. Write from_fahrenheit (no self), to_fahrenheit (&self), cool (&mut self, never below absolute zero; f64 has a max method) and colder (takes both values). colder is there to practise self; a real temperature type like this would be Copy.

Starterexercises/celsius.rs
struct Celsius(f64);
impl Celsius {
const ABSOLUTE_ZERO: f64 = -273.15;
/// An associated function: no `self`. Celsius = (F - 32) * 5 / 9.
fn from_fahrenheit(f: f64) -> Self {
todo!("Self((f - 32.0) * 5.0 / 9.0)")
}
/// Reads the temperature. Fahrenheit = C * 9 / 5 + 32.
fn to_fahrenheit(&self) -> f64 {
todo!("self.0 * 9.0 / 5.0 + 32.0")
}
/// Lowers the temperature, but never below ABSOLUTE_ZERO.
fn cool(&mut self, degrees: f64) {
todo!("subtract, then f64::max with Self::ABSOLUTE_ZERO")
}
/// Takes both temperatures and gives back the colder one.
fn colder(self, other: Celsius) -> Celsius {
todo!("compare self.0 and other.0")
}
}
fn main() {
assert_eq!(Celsius::from_fahrenheit(212.0).0, 100.0);
assert_eq!(Celsius::from_fahrenheit(-40.0).0, -40.0);
assert_eq!(Celsius(100.0).to_fahrenheit(), 212.0);
assert_eq!(Celsius(0.0).to_fahrenheit(), 32.0);
let mut c = Celsius(20.0);
c.cool(5.0);
assert_eq!(c.0, 15.0);
c.cool(1000.0);
assert_eq!(
c.0,
Celsius::ABSOLUTE_ZERO,
"nothing is colder than absolute zero"
);
let a = Celsius(3.0);
let b = Celsius(-4.5);
assert_eq!(a.colder(b).0, -4.5);
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/celsius.solution.rsRun ↗
struct Celsius(f64);
impl Celsius {
const ABSOLUTE_ZERO: f64 = -273.15;
/// An associated function: no `self`. Celsius = (F - 32) * 5 / 9.
fn from_fahrenheit(f: f64) -> Self {
Self((f - 32.0) * 5.0 / 9.0)
}
/// Reads the temperature. Fahrenheit = C * 9 / 5 + 32.
fn to_fahrenheit(&self) -> f64 {
self.0 * 9.0 / 5.0 + 32.0
}
/// Lowers the temperature, but never below ABSOLUTE_ZERO.
fn cool(&mut self, degrees: f64) {
self.0 = (self.0 - degrees).max(Self::ABSOLUTE_ZERO);
}
/// Takes both temperatures and gives back the colder one.
fn colder(self, other: Celsius) -> Celsius {
if self.0 <= other.0 { self } else { other }
}
}
fn main() {
assert_eq!(Celsius::from_fahrenheit(212.0).0, 100.0);
assert_eq!(Celsius::from_fahrenheit(-40.0).0, -40.0);
assert_eq!(Celsius(100.0).to_fahrenheit(), 212.0);
assert_eq!(Celsius(0.0).to_fahrenheit(), 32.0);
let mut c = Celsius(20.0);
c.cool(5.0);
assert_eq!(c.0, 15.0);
c.cool(1000.0);
assert_eq!(
c.0,
Celsius::ABSOLUTE_ZERO,
"nothing is colder than absolute zero"
);
let a = Celsius(3.0);
let b = Celsius(-4.5);
assert_eq!(a.colder(b).0, -4.5);
println!("all checks passed");
}
$ cargo run
all checks passed

Read more