RustArena

Structs and impl blocks in Rust: methods and associated functions

How to define a struct in Rust, add methods with impl, choose between &self, &mut self and self, and write a new() constructor.

A struct groups related values under one name, with a named field for each:

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

Create one by giving every field a value: Account { owner: String::from("Ferris"), balance: 0 }. If a variable has the same name as a field, you can write it once (field init shorthand): Account { owner, balance: 0 }. Read fields with a dot: acct.balance. To change a field, the whole variable must be let mut; Rust has no per-field mutability.

impl blocks: methods

Behavior goes in an impl block. A method is a function whose first parameter is some form of self, and you call it with a dot:

First parameter Meaning Called on Typical use
&self borrow the value to read any instance getters, calculations, formatting
&mut self borrow it to change it a let mut instance deposits, pushes, updates
self take ownership the instance is moved converting or consuming: into_*, finish, builder build

Most methods take &self or &mut self. Taking self means the caller can't use the value afterwards, which is exactly right for things like closing an account or turning a builder into the finished object.

Associated functions and new

A function in an impl block without self is an associated function, called with :: on the type. The idiomatic constructor is new:

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

let acct = Account::new("Ferris");

Inside an impl, Self is an alias for the type being implemented. Rust has no special constructor syntax; new is just a convention, and you can have others (with_capacity, from_parts, open). If a type has an obvious empty value, also derive or implement Default.

Printing a struct

println!("{}", acct) doesn't compile until you implement Display. For debugging, add #[derive(Debug)] above the struct and print with {:?} or the multi-line {:#?}. dbg!(&acct) prints the file, line and value to standard error.

Tuple structs and unit structs

struct Meters(f64); is a tuple struct: fields are accessed as .0, .1. A one-field tuple struct is a newtype, used to give a plain value its own type so you can't mix up Meters and Seconds. struct Marker; is a unit struct with no fields, useful as a type-level tag.

Common mistakes

At work

Keep fields private (no pub) when the struct has rules to protect, like "balance never goes negative", and expose methods that enforce them. Return Result from methods that can fail. Small getters returning &str or &[T] avoid needless clones.

Example

#[derive(Debug)]
struct Account {
    owner: String,
    balance: u64,
}

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

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

    fn withdraw(&mut self, amount: u64) -> Result<(), String> {
        if amount > self.balance {
            return Err(format!("insufficient funds: have {}, need {}", self.balance, amount));
        }
        self.balance -= amount;
        Ok(())
    }

    fn summary(&self) -> String {
        format!("{} has {} coins", self.owner, self.balance)
    }

    fn close(self) -> u64 {
        self.balance
    }
}

fn main() {
    let mut acct = Account::new("Ferris");
    acct.deposit(100);
    println!("{}", acct.summary());

    match acct.withdraw(250) {
        Ok(()) => println!("withdrew 250"),
        Err(e) => println!("error: {e}"),
    }
    acct.withdraw(30).expect("enough money");
    println!("{acct:?}");

    let paid_out = acct.close(); // acct is moved; it can't be used after this
    println!("closed, paid out {paid_out}");
}

Output:

Ferris has 100 coins
error: insufficient funds: have 100, need 250
Account { owner: "Ferris", balance: 70 }
closed, paid out 70

Practice it