Rust Fundamentals: Ownership, Errors and Fearless Concurrency · Rust Basics and Ownership

Structs and Methods

Group related data in structs, add behaviour with impl blocks (methods with &self, &mut self or self, and associated functions like new), and derive Debug, Clone and PartialEq.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A bank account type with a constructor, methods to deposit and withdraw, a method that consumes the account to close it, and debug printing.

The idea

A struct groups named fields into one type:

struct Account { owner: String, balance_cents: i64 }

Behaviour goes in an impl block:

  • fn new(...) -> Self: an associated function (no self), called as Account::new(...). new is a convention, not a keyword.
  • fn balance(&self): a method that reads the struct.
  • fn deposit(&mut self, ...): a method that changes it (the caller needs a mut variable).
  • fn close(self): a method that takes ownership, so the value can't be used afterwards.

Derive common behaviour with attributes: #[derive(Debug, Clone, PartialEq)] gives {:?} printing, .clone() and ==.

Fields are private to the module by default when the struct is used from other modules; expose methods rather than fields to protect invariants (like "balance can't go negative").

The code

An Account struct with an impl block
#[derive(Debug, Clone, PartialEq)]
struct Account {
    owner: String,
    balance_cents: i64,
}

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

    fn balance(&self) -> f64 {
        self.balance_cents as f64 / 100.0
    }

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

    fn withdraw(&mut self, cents: i64) -> bool {
        if cents > self.balance_cents {
            return false; // protect the invariant: no negative balance
        }
        self.balance_cents -= cents;
        true
    }

    fn close(self) -> String {
        format!("closed {}'s account, paid out {:.2}", self.owner, self.balance())
    }
}

fn main() {
    let mut acc = Account::new("Ana");
    acc.deposit(12_500);
    println!("balance: {:.2}", acc.balance());
    println!("withdraw 200.00: {}", acc.withdraw(20_000));
    println!("withdraw 30.00: {}", acc.withdraw(3_000));

    let snapshot = acc.clone();
    println!("{:?}", snapshot);
    println!("same as snapshot: {}", acc == snapshot);

    println!("{}", acc.close());
    // acc.deposit(100); // error: `acc` was moved by close()
}

Output

balance: 125.00
withdraw 200.00: false
withdraw 30.00: true
Account { owner: "Ana", balance_cents: 9500 }
same as snapshot: true
closed Ana's account, paid out 95.00

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Forgetting mut on the variable before calling a &mut self method.
  • Taking self by value by accident, which consumes the struct; use &self unless consuming is the point.
  • Public fields everywhere. Methods let you enforce rules (no negative balance).
  • Using floats for money. Store cents in an integer, as here.
  • Missing derives. {:?} needs Debug; == needs PartialEq.

Key points

  • struct groups fields; impl adds methods and associated functions.
  • &self reads, &mut self modifies, self consumes.
  • Self { ... } builds an instance; Type::new() is the constructor convention.
  • #[derive(Debug, Clone, PartialEq)] adds common behaviour.

Further reading: Using structs, Method syntax.

Practice questions

What's the difference between &self, &mut self and self in a method?

Answer

&self borrows the value read-only; &mut self borrows it mutably so the method can change fields (the caller needs a mutable binding); self takes ownership, so the value can't be used after the call.

What is an associated function?

Answer

A function in an impl block that doesn't take self, called with Type::function() (e.g. constructors like Account::new).

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?