Structs & Methods
A student has a name, a roll number, a city and a fee status. Keeping these in four separate variables quickly becomes a mess. A struct (short for structure) groups related values under one name, so you can pass a whole student around as one thing.
In this lesson you will define structs, create and update instances, print them for debugging, use tuple structs to make your types safer, and give structs their own functions called methods.
Defining and using a struct
A struct definition lists fields, each with a name and a type. You create an instance by giving every field a value:
#[derive(Debug)]
struct Student {
name: String,
roll_no: u32,
city: String,
fees_paid: bool,
}
fn main() {
let mut aarav = Student {
name: String::from("Aarav"),
roll_no: 17,
city: String::from("Rohtak"),
fees_paid: false,
};
aarav.fees_paid = true;
println!("{} (roll {}) from {}", aarav.name, aarav.roll_no, aarav.city);
println!("{aarav:?}");
println!("{aarav:#?}");
}Aarav (roll 17) from Rohtak
Student { name: "Aarav", roll_no: 17, city: "Rohtak", fees_paid: true }
Student {
name: "Aarav",
roll_no: 17,
city: "Rohtak",
fees_paid: true,
}- Struct names use PascalCase (
Student); field names use snake_case. - Read and write fields with a dot:
aarav.fees_paid. To change any field, the whole instance must bemut; Rust has no "mutable fields". - The struct owns its
Stringfields, so whenaaravis dropped, its name and city are freed too. #[derive(Debug)]asks the compiler to write the code that prints the struct with{:?}and{:#?}. Without it,{:?}does not compile. You will learn how to write a user-friendly{}format yourself in Traits.
Shorthands: field init and struct update
When a variable has the same name as a field, you can write the name once. This is the field init shorthand. The struct update syntax ..other fills every field you did not list from another instance:
// Student struct as above
fn new_student(name: &str, roll_no: u32) -> Student {
let city = String::from("Rohtak");
Student {
name: name.to_string(),
roll_no, // same as roll_no: roll_no
city, // same as city: city
fees_paid: false,
}
}
fn main() {
let isha = new_student("Isha", 18);
let kabir = Student {
name: String::from("Kabir"),
roll_no: 19,
..isha
};
println!("{} and {} both live in {}", isha.name, kabir.name, kabir.city);
}Isha and Kabir both live in Rohtak
Careful: ..isha moves isha.city (a String) into kabir. Afterwards isha.name is still usable, but isha.city is not, and isha as a whole cannot be used. Fields of Copy types such as fees_paid are simply copied.
Tuple structs and the newtype pattern
A tuple struct has a name but unnamed fields, which you read by position. A tuple struct with a single field is called a newtype. It costs nothing at run time, yet it stops you from mixing up values that happen to share a type:
#[derive(Debug)]
struct Rupees(u32);
#[derive(Debug)]
struct Kilometres(u32);
fn taxi_fare(distance: Kilometres) -> Rupees {
Rupees(50 + distance.0 * 18)
}
fn main() {
let fare = taxi_fare(Kilometres(12));
println!("{fare:?}");
println!("Pay Rs. {}", fare.0);
// taxi_fare(Rupees(12)); // error: expected `Kilometres`, found `Rupees`
}Rupees(266) Pay Rs. 266
With plain u32 parameters, passing a price where a distance was expected would compile and give a wrong bill. With newtypes, the compiler catches it. There are also unit-like structs with no fields at all (struct Offline;), useful as markers once you learn traits.
Methods with impl
Methods are functions that belong to a type. You write them inside an impl block. A method's first parameter is self, the instance it is called on, written in one of three forms:
| First parameter | Meaning | Caller can use the instance afterwards? |
|---|---|---|
&self | Borrows the instance to read it | Yes |
&mut self | Borrows the instance to change it | Yes |
self | Takes ownership of the instance | No, it has been moved |
Functions in an impl block without self are associated functions, called with Type::name(...). The usual constructor is an associated function named new. Inside an impl block, Self is a short name for the type.
struct Wallet {
owner: String,
balance: u32,
}
impl Wallet {
fn new(owner: &str) -> Self {
Self { owner: owner.to_string(), balance: 0 }
}
fn add_money(&mut self, amount: u32) {
self.balance += amount;
}
fn pay(&mut self, amount: u32) -> bool {
if amount > self.balance {
return false;
}
self.balance -= amount;
true
}
fn summary(&self) -> String {
format!("{} has Rs. {}", self.owner, self.balance)
}
fn close(self) -> u32 {
println!("Closing wallet of {}", self.owner);
self.balance
}
}
fn main() {
let mut wallet = Wallet::new("Priya");
wallet.add_money(500);
let paid_small = wallet.pay(180);
let paid_big = wallet.pay(400);
println!("Paid 180? {paid_small}. Paid 400? {paid_big}");
println!("{}", wallet.summary());
let refund = wallet.close();
println!("Refunded Rs. {refund}");
// wallet.summary(); // error: `wallet` was moved by `close`
}Paid 180? true. Paid 400? false Priya has Rs. 320 Closing wallet of Priya Refunded Rs. 320
You call methods with a dot, and Rust adds the & or &mut for you: wallet.add_money(500) really means Wallet::add_money(&mut wallet, 500). close takes self, so it consumes the wallet. That is a good design for actions after which the object must not be used again.
A type can have several impl blocks, and methods may share names with fields (a method balance(&self) next to a field balance is common for read-only access).
Common mistakes
- Trying to make one field mutable. Mutability belongs to the binding: declare the whole instance with
let mut. - Forgetting
#[derive(Debug)]. Without it, printing with{:?}fails with "doesn't implementDebug". - Using
selfwhen&selfis enough. A method takingselfmoves the instance, so the caller loses it. Read-only methods should take&self. - Forgetting that
..othermoves fields. If you still need the old instance, clone the fields you take.
Key points
- A struct groups named fields; create instances with
Name { field: value, ... }and use the fields with a dot. #[derive(Debug)]enables{:?}and{:#?}printing;dbg!is handy for quick checks.- Field init shorthand and
..otherupdate syntax reduce repetition, but..othercan move fields. - Tuple structs and newtypes like
Rupees(u32)make the compiler catch mixed-up values. - Methods live in
implblocks and take&self,&mut selforself;newis the usual constructor.
Exercise
Create a struct Book for a library in Pune with fields title: String, author: String and copies: u32. Add new(title: &str, author: &str, copies: u32) -> Self, a method lend(&mut self) -> bool that reduces copies if at least one is available, and a method label(&self) -> String that returns text like "Godaan by Premchand (2 left)". Lend the book three times when it has 2 copies and print the results.
Show solution
struct Book {
title: String,
author: String,
copies: u32,
}
impl Book {
fn new(title: &str, author: &str, copies: u32) -> Self {
Self { title: title.to_string(), author: author.to_string(), copies }
}
fn lend(&mut self) -> bool {
if self.copies == 0 {
return false;
}
self.copies -= 1;
true
}
fn label(&self) -> String {
format!("{} by {} ({} left)", self.title, self.author, self.copies)
}
}
fn main() {
let mut book = Book::new("Godaan", "Premchand", 2);
println!("{}", book.label());
for _ in 0..3 {
println!("Lent: {}", book.lend());
}
println!("{}", book.label());
}Godaan by Premchand (2 left) Lent: true Lent: true Lent: false Godaan by Premchand (0 left)
lend takes &mut self because it changes copies, while label only reads, so it takes &self. The _ in the loop means the loop counter is not needed.
Quick Quiz
1. What does #[derive(Debug)] do for a struct?
Deriving Debug implements the Debug formatting trait automatically.
2. Which first parameter should a method have if it only reads fields?
&self borrows the instance immutably, so the caller keeps it and nothing changes.
3. How do you call an associated function new on Wallet?
Associated functions have no self and are called on the type with ::.
4. What is the benefit of struct Rupees(u32); over a plain u32?
A newtype is a distinct type, so mixing it up with other numbers is a compile error, at no run-time cost.
Learn Rust with a mentor
Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
