Structs, Enums and Pattern Matching · about 40 min

Methods and impl Blocks

Why this matters

A struct describes data. Most data comes with operations: a rectangle has an area, an account can be deposited into, a counter can tick. Methods attach functions to a type, so related code lives together and calls read naturally: account.deposit(100) instead of deposit(&mut account, 100). In Rust, a method's first parameter also states, in the signature, what it does with ownership: read, modify, or consume. That makes methods a direct application of everything from modules 4 and 5.

impl blocks and self

#[derive(Debug)]
struct Rect {
    width: u32,
    height: u32,
}

impl Rect {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rect) -> bool {
        self.width >= other.width && self.height >= other.height
    }

    fn scale(&mut self, factor: u32) {
        self.width *= factor;
        self.height *= factor;
    }
}

fn main() {
    let mut big = Rect { width: 10, height: 8 };
    let small = Rect { width: 3, height: 4 };
    println!("area {}", big.area());
    println!("holds small: {}", big.can_hold(&small));
    big.scale(2);
    println!("{:?}", big);
}
area 80
holds small: true
Rect { width: 20, height: 16 }

The three receivers

The form of self is an ownership decision, just like any parameter:

receivermeanscaller afterwardstypical use
&selfborrow to readstill owns, unchangedgetters, queries, computations
&mut selfborrow to modifystill owns, changedsetters, updates
selftake ownershipvalue is moved, goneconversions, finishing a builder
struct Draft {
    text: String,
}

struct Post {
    text: String,
}

impl Draft {
    fn add(&mut self, s: &str) {
        self.text.push_str(s);
    }

    fn word_count(&self) -> usize {
        self.text.split_whitespace().count()
    }

    fn publish(self) -> Post {
        Post { text: self.text }
    }
}

fn main() {
    let mut d = Draft { text: String::new() };
    d.add("Hello ");
    d.add("methods");
    println!("{} words", d.word_count());
    let p = d.publish();
    println!("published: {}", p.text);
}
2 words
published: Hello methods

publish(self) moves the draft into the method, which moves its String into the new Post, with no copying. After it, d is gone: try d.add("more") and you get E0382 with the note "Draft::publish takes ownership of the receiver self, which moves d". The type system now models the workflow: a published post can no longer be edited as a draft.

Automatic referencing

When you call big.area(), Rust adds &, &mut or * as needed to match the method's receiver. So big.area() means Rect::area(&big), and big.scale(2) means Rect::scale(&mut big, 2). That is why method calls never need (&big).area(). You can write the fully qualified form too, which shows that methods are just functions:

struct Rect {
    width: u32,
    height: u32,
}

impl Rect {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let r = Rect { width: 2, height: 5 };
    let by_method = r.area();
    let by_function = Rect::area(&r);
    println!("{by_method} {by_function}");
}
10 10

The borrowing rules apply unchanged: calling a &mut self method while holding a reference into the struct is E0502, and calling one on an immutable binding is E0596.

Associated functions and constructors

Functions in an impl block without self are associated functions, called with Type::name(). String::from and String::new are examples. The conventional constructor is named new:

#[derive(Debug)]
struct Temperature {
    celsius: f64,
}

impl Temperature {
    fn new(celsius: f64) -> Self {
        Self { celsius }
    }

    fn from_fahrenheit(f: f64) -> Self {
        Self::new((f - 32.0) * 5.0 / 9.0)
    }

    fn fahrenheit(&self) -> f64 {
        self.celsius * 9.0 / 5.0 + 32.0
    }
}

fn main() {
    let t = Temperature::from_fahrenheit(212.0);
    println!("{:?} = {} F", t, t.fahrenheit());
}
Temperature { celsius: 100.0 } = 212 F

Self (capital S) inside an impl block is an alias for the type being implemented, here Temperature. Rust has no special constructor syntax; new is just a convention, and you can offer several named constructors like from_fahrenheit. Constructors are a good place to enforce rules (for example, rejecting a negative width), which pairs with privacy in module 12 so that nobody can bypass them.

Methods and field names

A method may have the same name as a field: fn width(&self) -> u32 { self.width }. r.width is the field, r.width() the method. This is the usual way to offer read-only access to private fields (module 12).

You can split an impl into several blocks; it makes no difference to the compiler. Later you will write impl SomeTrait for Type blocks too (module 10).

Common mistakes

Forgetting self: a function in an impl without self cannot be called with dot syntax:

struct Rect {
    width: u32,
    height: u32,
}

impl Rect {
    fn area(r: &Rect) -> u32 {
        r.width * r.height
    }
}

fn main() {
    let r = Rect { width: 2, height: 3 };
    println!("{}", r.area());
}
error[E0599]: no method named `area` found for struct `Rect` in the current scope
  --> main.rs:14:22
   |
1  | struct Rect {
   | ----------- method `area` not found for this struct
...
14 |     println!("{}", r.area());
   |                    --^^^^--
   |                    | |
   |                    | this is an associated function, not a method
   |                    help: use associated function syntax instead: `Rect::area(&r)`
   |
   = note: found the following associated functions; to be used as methods, functions must have a `self` parameter
note: the candidate is defined in an impl for the type `Rect`
  --> main.rs:7:5
   |
7  |     fn area(r: &Rect) -> u32 {
   |     ^^^^^^^^^^^^^^^^^^^^^^^^

E0599: "this is an associated function, not a method". Make the first parameter &self.

Calling a &mut self method on an immutable binding: E0596 "cannot borrow c as mutable". Declare it let mut.

Writing width instead of self.width inside a method: E0425 cannot find value width, with a help suggesting self.width.

Taking self when &self would do: the value is consumed by a mere query. Use self only when consuming is the point.

Recap

Going further: add a fn square(size: u32) -> Self constructor to Rect, and a method fn into_tuple(self) -> (u32, u32). Then read the Rust Book chapter 5.3, "Method Syntax".

Exercises

Exercise 1 · write code

A counter type

Implement methods for struct Counter { count: u32, step: u32 }:

The given main prints:

15
15
0

File: main.rs

Starting point

struct Counter {
    count: u32,
    step: u32,
}

fn main() {
    let mut c = Counter::new(5);
    c.tick();
    c.tick();
    c.tick();
    println!("{}", c.value());
    println!("{}", c.reset());
    println!("{}", c.value());
}

Example

expected output

15
15
0
Hint 1

All four go in one impl Counter { ... } block.

Hint 2

new has no self parameter; the others take &self or &mut self and use self.count.

Reference solution
struct Counter {
    count: u32,
    step: u32,
}

impl Counter {
    fn new(step: u32) -> Counter {
        Counter { count: 0, step }
    }

    fn tick(&mut self) {
        self.count += self.step;
    }

    fn value(&self) -> u32 {
        self.count
    }

    fn reset(&mut self) -> u32 {
        let old = self.count;
        self.count = 0;
        old
    }
}

fn main() {
    let mut c = Counter::new(5);
    c.tick();
    c.tick();
    c.tick();
    println!("{}", c.value());
    println!("{}", c.reset());
    println!("{}", c.value());
}

Exercise 2 · quiz

Which receiver?

impl Document { fn publish(self) -> Published { ... } }. After let p = doc.publish();, what is true?

Answer
  • doc has been moved into publish and cannot be used any more

A self receiver (no &) takes ownership, exactly like a function parameter of type Document. That is useful for transitions where the old value should no longer exist: a draft becomes a published document, a builder becomes the thing it built. The compiler then prevents accidental use of the stale draft.

Exercise 3 · write code

Bank account from commands

Implement struct Account { owner: String, balance: i64 } (cents) with:

The given main reads commands from stdin (deposit N, withdraw N, amounts in cents) and prints declined for refused withdrawals and the summary at the end. For

deposit 1000
withdraw 250
withdraw 5000
deposit 500

it prints

declined
Ada: 12.50

File: main.rs

Starting point

use std::io;

fn main() {
    let mut acct = Account::new("Ada");
    for line in io::stdin().lines() {
        let line = line.unwrap();
        let mut parts = line.split_whitespace();
        let cmd = parts.next().unwrap();
        let amount: i64 = parts.next().unwrap().parse().unwrap();
        match cmd {
            "deposit" => acct.deposit(amount),
            "withdraw" => {
                if !acct.withdraw(amount) {
                    println!("declined");
                }
            }
            _ => println!("unknown"),
        }
    }
    println!("{}", acct.summary());
}

Example

input

deposit 1000
withdraw 250
withdraw 5000
deposit 500

expected output

declined
Ada: 12.50

Example

input

deposit 305
withdraw 305

expected output

Ada: 0.00
Hint 1

Keep money in integer cents. For the summary, dollars are balance / 100 and cents balance % 100 printed with {:02}.

Hint 2

withdraw returns false without changing anything when amount > self.balance.

Reference solution
use std::io;

struct Account {
    owner: String,
    balance: i64,
}

impl Account {
    fn new(owner: &str) -> Self {
        Self { owner: owner.to_string(), balance: 0 }
    }

    fn deposit(&mut self, amount: i64) {
        self.balance += amount;
    }

    fn withdraw(&mut self, amount: i64) -> bool {
        if amount > self.balance {
            return false;
        }
        self.balance -= amount;
        true
    }

    fn summary(&self) -> String {
        format!("{}: {}.{:02}", self.owner, self.balance / 100, self.balance % 100)
    }
}

fn main() {
    let mut acct = Account::new("Ada");
    for line in io::stdin().lines() {
        let line = line.unwrap();
        let mut parts = line.split_whitespace();
        let cmd = parts.next().unwrap();
        let amount: i64 = parts.next().unwrap().parse().unwrap();
        match cmd {
            "deposit" => acct.deposit(amount),
            "withdraw" => {
                if !acct.withdraw(amount) {
                    println!("declined");
                }
            }
            _ => println!("unknown"),
        }
    }
    println!("{}", acct.summary());
}