Skip to content

Rust Structs

A struct groups related values into one named type. Where a tuple is a quick, unnamed bundle, a struct gives every field a name — which makes the code that uses it far easier to read.

struct Person {
name: String,
age: u32,
}
fn main() {
let alice = Person {
name: String::from("Alice"),
age: 30,
};
println!("{} is {}", alice.name, alice.age);
}

Output:

Alice is 30
▶ Try it Yourself

Compare this to a tuple ("Alice", 30) — with a struct, alice.name and alice.age are self-explanatory. You don’t have to remember that field 0 is the name and field 1 is the age.

Like variables, struct instances are immutable by default. Add mut to change a field after creating it:

struct Person {
name: String,
age: u32,
}
fn main() {
let mut alice = Person {
name: String::from("Alice"),
age: 30,
};
alice.age += 1;
println!("{} is now {}", alice.name, alice.age);
}
▶ Try it Yourself

You can attach functions to a struct using an impl block. Inside, &self refers to the specific instance the method was called on:

struct Person {
name: String,
age: u32,
}
impl Person {
fn greet(&self) {
println!("Hi, I'm {} and I'm {}", self.name, self.age);
}
}
fn main() {
let alice = Person { name: String::from("Alice"), age: 30 };
alice.greet();
}

Output:

Hi, I'm Alice and I'm 30
▶ Try it Yourself

A common pattern is a new function inside impl that builds an instance for you, so callers don’t have to write out every field by hand:

struct Person {
name: String,
age: u32,
}
impl Person {
fn new(name: &str, age: u32) -> Person {
Person { name: name.to_string(), age }
}
}
fn main() {
let bob = Person::new("Bob", 25);
println!("{} is {}", bob.name, bob.age);
}
▶ Try it Yourself

Notice age on its own inside Person { name: name.to_string(), age } — when a variable has the exact same name as the field, you can skip writing age: age and just write age. Rust calls this field init shorthand.

Try println!("{:?}", alice) on a struct as written above, and it won’t compile — Rust doesn’t know how to debug-print a custom type unless you tell it to. Add #[derive(Debug)] above the struct to get that for free:

#[derive(Debug)]
struct Person {
name: String,
age: u32,
}
fn main() {
let alice = Person { name: String::from("Alice"), age: 30 };
println!("{:?}", alice);
}

Output:

Person { name: "Alice", age: 30 }
▶ Try it Yourself

#[derive(...)] is Rust’s way of auto-generating common functionality for a type — you’ll see Debug, and later Clone and others, added this way constantly.

Make a Rectangle struct with width and height fields (u32). Give it two methods: area, which returns the area, and is_square, which returns true when both sides are equal. Hint: both methods only read the fields, so they take &self.

Show solution
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn is_square(&self) -> bool {
self.width == self.height
}
}
fn main() {
let wide = Rectangle { width: 8, height: 3 };
let square = Rectangle { width: 5, height: 5 };
println!("wide: area {}, square? {}", wide.area(), wide.is_square());
println!("square: area {}, square? {}", square.area(), square.is_square());
}

Output:

wide: area 24, square? false
square: area 25, square? true
▶ Try it Yourself

Make an Account struct with an owner (String) and a balance in cents (u32). Write Account::new("Ana") that starts the balance at 0. Then deposit 50 and 25 cents by changing the field, and print the account in two ways: with a normal sentence and with {:?}. Hint: the variable has to be let mut before you can change account.balance, and {:?} needs #[derive(Debug)].

Show solution
#[derive(Debug)]
struct Account {
owner: String,
balance: u32,
}
impl Account {
fn new(owner: &str) -> Account {
Account { owner: owner.to_string(), balance: 0 }
}
}
fn main() {
let mut account = Account::new("Ana");
account.balance += 50;
account.balance += 25;
println!("{} has {} cents", account.owner, account.balance);
println!("{:?}", account);
}

Output:

Ana has 75 cents
Account { owner: "Ana", balance: 75 }
▶ Try it Yourself

new sets balance: 0 itself, so callers can’t forget it. That’s a big part of why constructor functions are so common.

Put three Person values in a vector: Ana (34), Ben (16) and Chen (52). Print the name of everyone who is 18 or older, then print who is the oldest. Hint: loop with for person in &people, and keep track of the oldest person so far with let mut oldest = &people[0];.

Show solution
struct Person {
name: String,
age: u32,
}
fn main() {
let people = vec![
Person { name: String::from("Ana"), age: 34 },
Person { name: String::from("Ben"), age: 16 },
Person { name: String::from("Chen"), age: 52 },
];
let mut oldest = &people[0];
for person in &people {
if person.age >= 18 {
println!("{} is an adult", person.name);
}
if person.age > oldest.age {
oldest = person;
}
}
println!("Oldest: {} ({})", oldest.name, oldest.age);
}

Output:

Ana is an adult
Chen is an adult
Oldest: Chen (52)
▶ Try it Yourself

oldest is a reference (&Person) into the vector, not a copy, so nothing gets cloned. Each time we find someone older, we just point oldest at them instead.

  • A struct groups named fields into one type: struct Person { name: String, age: u32 }.
  • Create an instance with Person { name: ..., age: ... }; mark it mut to change fields later.
  • Add behavior with an impl block; &self refers to the instance a method was called on.
  • Add #[derive(Debug)] to make a struct printable with {:?}.

Quick check

1. What's the main advantage of a struct over a tuple?

2. What does adding #[derive(Debug)] to a struct let you do?

3. In Person { name: name.to_string(), age }, what does the bare age do?

Score: 0 / 3