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
- Missing a field:
Point { x: 1 }is E0063, "missing fieldyin initializer ofPoint". Every field must be set (struct update syntax..Default::default()fills the rest). - Calling a
&mut selfmethod on an immutable variable: E0596, "cannot borrowacctas mutable, as it is not declared as mutable". Addmut. - Changing a field in a
&selfmethod: E0594, "cannot assign toself.balance, which is behind a&reference". Make it&mut self. - Calling an associated function with a dot (
acct.new(...)) or a method with::without passing the instance.Account::summary(&acct)works, butacct.summary()is the normal form.
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