Lesson 19 of 94
Structs
Rust has no classes. A struct holds data, and methods are attached separately in an impl block (the next lesson). There is no inheritance; instead, you combine structs and share behavior with traits.
This lesson is about the data side: defining structs, creating them, and how ownership and borrowing apply to their fields.
| Concept | Rust |
|---|---|
| Named-field struct | struct User { name: String, age: u32 } |
| Small data type | struct Point { x: i32, y: i32 } or struct Point(i32, i32) |
| Creating an instance | User { name: String::from("Ana"), age: 30 } |
| Debug printing | #[derive(Debug)] and {:?} |
| Mutability | The whole instance is immutable unless the variable is mut |
| Memory layout | A struct is just its fields, stored inline (stack, or inside another value) |
1. Defining and creating a struct
struct User {
username: String,
email: String,
age: u32,
active: bool,
}
fn main() {
let user = User {
username: String::from("ferris"),
email: String::from("ferris@example.com"),
age: 8,
active: true,
};
println!("{} <{}> age {} active {}", user.username, user.email, user.age, user.active);
}ferris <ferris@example.com> age 8 active trueFields are set by name, in any order, and every field must be given a value: there is no default null or 0.
2. Forgetting a field is a compile error
struct User {
username: String,
email: String,
age: u32,
}
fn main() {
let user = User {
username: String::from("ferris"),
age: 8,
};
println!("{}", user.username);
}error[E0063]: missing field `email` in initializer of `User`
--> src/main.rs:8:16
|
8 | let user = User {
| ^^^^ missing `email`In Rust, an instance is always complete.
3. Printing a struct with #[derive(Debug)]
A struct cannot be printed with {} or {:?} until you ask for it. The line #[derive(Debug)] above the struct tells the compiler to generate debug printing. {:#?} pretty-prints over several lines.
#[derive(Debug)]
struct Product {
sku: String,
price_cents: u32,
in_stock: bool,
}
fn main() {
let p = Product { sku: String::from("KB-001"), price_cents: 4999, in_stock: true };
println!("{:?}", p);
println!("{:#?}", p);
}Product { sku: "KB-001", price_cents: 4999, in_stock: true }
Product {
sku: "KB-001",
price_cents: 4999,
in_stock: true,
}#[...] is an attribute. derive generates code for a standard trait. You will see several more derives in this lesson; Deriving Standard Traits explains them all.
4. Without derive(Debug), printing does not compile
struct Product {
sku: String,
}
fn main() {
let p = Product { sku: String::from("KB-001") };
println!("{:?}", p);
}error[E0277]: `Product` doesn't implement `Debug`
--> src/main.rs:7:22
|
7 | println!("{:?}", p);
| ---- ^ `Product` cannot be formatted using `{:?}` because it doesn't implement `Debug`
| |
| required by this formatting parameter
|
= help: the trait `Debug` is not implemented for `Product`
= note: add `#[derive(Debug)]` to `Product` or manually `impl Debug for Product`
help: consider annotating `Product` with `#[derive(Debug)]`
|
1 + #[derive(Debug)]
2 | struct Product {
|5. Mutability applies to the whole instance
There is no per-field mut. If the variable is mut, every field can change; otherwise none can.
#[derive(Debug)]
struct Counter {
name: String,
count: u64,
}
fn main() {
let mut c = Counter { name: String::from("page_views"), count: 0 };
c.count += 1;
c.count += 1;
c.name.push_str("_total");
println!("{:?}", c);
}Counter { name: "page_views_total", count: 2 }Changing a field of an immutable instance is an error:
struct Counter {
count: u64,
}
fn main() {
let c = Counter { count: 0 };
c.count += 1;
println!("{}", c.count);
}error[E0594]: cannot assign to `c.count`, as `c` is not declared as mutable
--> src/main.rs:7:5
|
7 | c.count += 1;
| ^^^^^^^^^^^^ cannot assign
|
help: consider changing this to be mutable
|
6 | let mut c = Counter { count: 0 };
| +++6. A constructor function with the field init shorthand
Rust has no constructors. By convention you write a plain function (or an associated function called new, next lesson). When a variable has the same name as a field, you can write the name once.
#[derive(Debug)]
struct User {
username: String,
email: String,
active: bool,
}
fn build_user(username: String, email: String) -> User {
User {
username, // shorthand for `username: username`
email,
active: true,
}
}
fn main() {
let user = build_user(String::from("ada"), String::from("ada@example.com"));
println!("{:?}", user);
}User { username: "ada", email: "ada@example.com", active: true }7. Struct update syntax
..other fills in the remaining fields from another instance. It is handy for "same as this one, except...".
#[derive(Debug)]
struct Settings {
theme: String,
font_size: u32,
autosave: bool,
language: String,
}
fn main() {
let defaults = Settings {
theme: String::from("light"),
font_size: 14,
autosave: true,
language: String::from("en"),
};
let dark = Settings {
theme: String::from("dark"),
font_size: 16,
..defaults
};
println!("{:?}", dark);
println!("defaults font size is still readable: {}", defaults.font_size);
}Settings { theme: "dark", font_size: 16, autosave: true, language: "en" }
defaults font size is still readable: 148. Update syntax moves the String fields
..defaults moves every field it copies over. The String field language moved into dark, so defaults is now partially moved and cannot be used as a whole.
#[derive(Debug)]
struct Settings {
theme: String,
font_size: u32,
language: String,
}
fn main() {
let defaults = Settings {
theme: String::from("light"),
font_size: 14,
language: String::from("en"),
};
let dark = Settings { theme: String::from("dark"), ..defaults };
println!("{:?}", dark);
println!("{:?}", defaults);
}error[E0382]: borrow of partially moved value: `defaults`
--> src/main.rs:16:22
|
14 | let dark = Settings { theme: String::from("dark"), ..defaults };
| ---------------------------------------------------- value partially moved here
15 | println!("{:?}", dark);
16 | println!("{:?}", defaults);
| ^^^^^^^^ value borrowed here after partial move
|
= note: partial move occurs because `defaults.language` has type `String`, which does not implement the `Copy` traitFields that were not taken (here theme, which dark set itself) and Copy fields (like font_size) can still be read individually, as example 7 did. To keep defaults whole, write ..defaults.clone() with #[derive(Clone)].
9. Tuple structs
A tuple struct has a name but unnamed fields, accessed with .0, .1. Use it when the field names would add nothing.
#[derive(Debug)]
struct Rgb(u8, u8, u8);
#[derive(Debug)]
struct Point3(f64, f64, f64);
fn main() {
let orange = Rgb(255, 165, 0);
let origin = Point3(0.0, 0.0, 0.0);
println!("{:?} red={}", orange, orange.0);
println!("{:?}", origin);
}Rgb(255, 165, 0) red=255
Point3(0.0, 0.0, 0.0)10. The newtype pattern: types that prevent mix-ups
A tuple struct with one field creates a distinct type wrapping another. It costs nothing at runtime, and the compiler stops you from mixing up values that happen to have the same underlying type.
struct Meters(f64);
struct Feet(f64);
fn set_altitude(altitude: Meters) {
println!("altitude set to {} m", altitude.0);
}
fn main() {
let reading = Feet(35_000.0);
set_altitude(reading);
}error[E0308]: mismatched types
--> src/main.rs:10:18
|
10 | set_altitude(reading);
| ------------ ^^^^^^^ expected `Meters`, found `Feet`
| |
| arguments to this function are incorrect
|
note: function defined here
--> src/main.rs:4:4
|
4 | fn set_altitude(altitude: Meters) {
| ^^^^^^^^^^^^ ----------------Without the newtypes, both would be plain f64, and passing feet where meters are expected would compile fine. (NASA lost the Mars Climate Orbiter to exactly this kind of unit mix-up.) The fix is an explicit conversion:
struct Meters(f64);
struct Feet(f64);
fn feet_to_meters(f: Feet) -> Meters {
Meters(f.0 * 0.3048)
}
fn set_altitude(altitude: Meters) {
println!("altitude set to {:.1} m", altitude.0);
}
fn main() {
let reading = Feet(35_000.0);
set_altitude(feet_to_meters(reading));
}altitude set to 10668.0 m11. Newtypes for IDs
Wrapping IDs stops you from passing an order ID where a customer ID belongs.
#[derive(Debug, Clone, Copy, PartialEq)]
struct CustomerId(u64);
#[derive(Debug, Clone, Copy, PartialEq)]
struct OrderId(u64);
fn cancel_order(customer: CustomerId, order: OrderId) {
println!("customer {} cancels order {}", customer.0, order.0);
}
fn main() {
let customer = CustomerId(42);
let order = OrderId(9001);
cancel_order(customer, order);
// cancel_order(order, customer); // does not compile: the types are swapped
println!("{:?} == {:?}: {}", customer, CustomerId(42), customer == CustomerId(42));
}customer 42 cancels order 9001
CustomerId(42) == CustomerId(42): trueClone, Copy, PartialEq in the derive list make these IDs copyable like numbers and comparable with ==.
12. Unit structs
A struct with no fields at all. It takes zero bytes. It is useful as a marker or as something to attach behavior to, which you will see with traits.
#[derive(Debug)]
struct Heartbeat;
fn main() {
let h = Heartbeat;
println!("{:?} takes {} bytes", h, std::mem::size_of::<Heartbeat>());
}Heartbeat takes 0 bytes13. Nested structs
#[derive(Debug)]
struct Address {
street: String,
city: String,
postcode: String,
}
#[derive(Debug)]
struct Customer {
name: String,
shipping: Address,
billing: Address,
}
fn main() {
let home = Address {
street: String::from("1 Main St"),
city: String::from("Springfield"),
postcode: String::from("12345"),
};
let office = Address {
street: String::from("500 Market St"),
city: String::from("Capital City"),
postcode: String::from("67890"),
};
let customer = Customer { name: String::from("Homer"), shipping: home, billing: office };
println!("{} ships to {}, {}", customer.name, customer.shipping.street, customer.shipping.city);
println!("{} bills to {} {}", customer.name, customer.billing.city, customer.billing.postcode);
}Homer ships to 1 Main St, Springfield
Homer bills to Capital City 67890A Customer contains both addresses inline: the struct is its fields. The strings inside still live on the heap.
14. A struct owns its fields
When a struct is dropped, all of its fields are dropped. Moving values into a struct moves them, just like pushing into a Vec.
struct Email {
to: String,
subject: String,
}
fn main() {
let recipient = String::from("ops@example.com");
let email = Email { to: recipient, subject: String::from("Disk full") };
println!("sending to {recipient}");
println!("{}", email.subject);
}error[E0382]: borrow of moved value: `recipient`
--> src/main.rs:9:27
|
7 | let recipient = String::from("ops@example.com");
| --------- move occurs because `recipient` has type `String`, which does not implement the `Copy` trait
8 | let email = Email { to: recipient, subject: String::from("Disk full") };
| --------- value moved here
9 | println!("sending to {recipient}");
| ^^^^^^^^^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
8 | let email = Email { to: recipient.clone(), subject: String::from("Disk full") };
| ++++++++15. Borrowing a struct in a function
Functions that only read a struct take &Struct. Functions that change it take &mut Struct.
#[derive(Debug)]
struct Account {
owner: String,
balance_cents: i64,
}
fn describe(account: &Account) -> String {
format!("{}: {:.2}", account.owner, account.balance_cents as f64 / 100.0)
}
fn deposit(account: &mut Account, cents: i64) {
account.balance_cents += cents;
}
fn main() {
let mut acc = Account { owner: String::from("Ana"), balance_cents: 10_000 };
deposit(&mut acc, 2_550);
println!("{}", describe(&acc));
deposit(&mut acc, -500);
println!("{}", describe(&acc));
}Ana: 125.50
Ana: 120.50Field access through a reference (account.owner) dereferences automatically; there is no -> operator as in C++.
16. Borrowing two fields at once
Different fields are different values, so you can borrow them separately, even mutably.
#[derive(Debug)]
struct Game {
player_score: u32,
enemy_score: u32,
log: Vec<String>,
}
fn main() {
let mut game = Game { player_score: 0, enemy_score: 0, log: Vec::new() };
let score = &mut game.player_score;
let log = &mut game.log;
*score += 10;
log.push(format!("player scored, now {score}"));
println!("{:?}", game);
}Game { player_score: 10, enemy_score: 0, log: ["player scored, now 10"] }17. Moving a field out of a struct
Like tuples, you can move one field out. The struct becomes partially moved: other fields still work, but the struct as a whole does not.
struct Upload {
filename: String,
bytes: Vec<u8>,
size: usize,
}
fn main() {
let upload = Upload { filename: String::from("photo.jpg"), bytes: vec![0xFF, 0xD8, 0xFF], size: 3 };
let data = upload.bytes; // move the buffer out
println!("{} ({} bytes)", upload.filename, upload.size);
println!("first byte: {:#X}", data[0]);
}photo.jpg (3 bytes)
first byte: 0xFF18. Destructuring a struct
A let can take a struct apart into variables, like a tuple. .. ignores the remaining fields.
struct Request {
method: String,
path: String,
body: String,
timeout_ms: u32,
}
fn main() {
let req = Request {
method: String::from("POST"),
path: String::from("/api/orders"),
body: String::from("{}"),
timeout_ms: 5_000,
};
let Request { method, path, .. } = req;
println!("{method} {path}");
let other = Request { method: String::from("GET"), path: String::from("/"), body: String::new(), timeout_ms: 100 };
let Request { timeout_ms: limit, .. } = other; // rename while destructuring
println!("timeout {limit}");
}POST /api/orders
timeout 10019. Destructuring a borrowed struct
Destructuring a reference gives references to the fields, so nothing is moved.
struct Point {
x: i32,
y: i32,
}
fn manhattan(&Point { x, y }: &Point) -> i32 {
x.abs() + y.abs()
}
fn main() {
let p = Point { x: -3, y: 4 };
let Point { x, y } = &p;
println!("x={x} y={y}, distance={}", manhattan(&p));
println!("p is still usable: ({}, {})", p.x, p.y);
}x=-3 y=4, distance=7
p is still usable: (-3, 4)20. A Vec of structs
#[derive(Debug)]
struct Task {
title: String,
done: bool,
estimate_hours: f32,
}
fn main() {
let mut tasks = vec![
Task { title: String::from("design schema"), done: true, estimate_hours: 3.0 },
Task { title: String::from("write migrations"), done: false, estimate_hours: 2.5 },
Task { title: String::from("load test"), done: false, estimate_hours: 5.0 },
];
tasks[1].done = true;
let mut remaining = 0.0;
for task in &tasks {
let mark = if task.done { "x" } else { " " };
println!("[{mark}] {}", task.title);
if !task.done {
remaining += task.estimate_hours;
}
}
println!("remaining: {remaining} hours");
}[x] design schema
[x] write migrations
[ ] load test
remaining: 5 hours21. Struct memory layout
A Rust struct has no object header, no class pointer, and no lock word. Its size is the size of its fields, plus padding for alignment.
struct Pixel {
r: u8,
g: u8,
b: u8,
a: u8,
}
struct Reading {
sensor_id: u32,
value: f64,
}
fn main() {
println!("Pixel: {} bytes", std::mem::size_of::<Pixel>());
println!("Reading: {} bytes", std::mem::size_of::<Reading>());
let row = [Pixel { r: 255, g: 0, b: 0, a: 255 }, Pixel { r: 0, g: 0, b: 255, a: 128 }];
println!("two pixels: {} bytes", std::mem::size_of_val(&row));
println!("first pixel is red: {}", row[0].r == 255 && row[0].g == 0 && row[0].b == 0 && row[0].a == 255);
println!("second pixel is half transparent: {}", row[1].a == 128);
}Pixel: 4 bytes
Reading: 16 bytes
two pixels: 8 bytes
first pixel is red: true
second pixel is half transparent: trueA Vec<Pixel> of a million pixels is exactly 4 MB of contiguous memory. (Reading is 16 bytes, not 12, because the f64 must start at a multiple of 8.)
22. Copyable structs with #[derive(Clone, Copy)]
If every field is Copy, you can make the struct Copy too. Then assignment copies it instead of moving it, like an integer.
#[derive(Debug, Clone, Copy)]
struct Vec2 {
x: f64,
y: f64,
}
fn add(a: Vec2, b: Vec2) -> Vec2 {
Vec2 { x: a.x + b.x, y: a.y + b.y }
}
fn main() {
let position = Vec2 { x: 1.0, y: 2.0 };
let velocity = Vec2 { x: 0.5, y: -1.0 };
let next = add(position, velocity);
println!("{:?} + {:?} = {:?}", position, velocity, next); // both still usable
}Vec2 { x: 1.0, y: 2.0 } + Vec2 { x: 0.5, y: -1.0 } = Vec2 { x: 1.5, y: 1.0 }A struct with a String field cannot be Copy, for the reasons in Move, Copy & Clone.
23. Cloning and comparing structs
#[derive(Clone)] generates a deep clone(). #[derive(PartialEq)] generates ==, comparing field by field.
#[derive(Debug, Clone, PartialEq)]
struct Config {
host: String,
port: u16,
}
fn main() {
let original = Config { host: String::from("localhost"), port: 8080 };
let mut copy = original.clone();
println!("equal after clone: {}", original == copy);
copy.port = 9090;
println!("equal after change: {}", original == copy);
println!("{:?} vs {:?}", original, copy);
}equal after clone: true
equal after change: false
Config { host: "localhost", port: 8080 } vs Config { host: "localhost", port: 9090 }24. Default values with #[derive(Default)]
Default gives every field its default value: 0, false, an empty String or Vec. Combined with update syntax, it gives you "named arguments with defaults".
#[derive(Debug, Default)]
struct HttpOptions {
timeout_ms: u32,
retries: u8,
follow_redirects: bool,
user_agent: String,
}
fn main() {
let all_defaults = HttpOptions::default();
let custom = HttpOptions {
retries: 3,
user_agent: String::from("my-crawler/1.0"),
..Default::default()
};
println!("{:?}", all_defaults);
println!("{:?}", custom);
}HttpOptions { timeout_ms: 0, retries: 0, follow_redirects: false, user_agent: "" }
HttpOptions { timeout_ms: 0, retries: 3, follow_redirects: false, user_agent: "my-crawler/1.0" }25. Realistic example: an order with line items
#[derive(Debug)]
struct LineItem {
product: String,
unit_price_cents: u64,
quantity: u32,
}
#[derive(Debug)]
struct Order {
id: u32,
customer: String,
items: Vec<LineItem>,
discount_percent: u8,
}
fn subtotal_cents(order: &Order) -> u64 {
let mut total = 0;
for item in &order.items {
total += item.unit_price_cents * item.quantity as u64;
}
total
}
fn total_cents(order: &Order) -> u64 {
let sub = subtotal_cents(order);
sub - sub * order.discount_percent as u64 / 100
}
fn main() {
let order = Order {
id: 5012,
customer: String::from("acme"),
items: vec![
LineItem { product: String::from("widget"), unit_price_cents: 1_250, quantity: 4 },
LineItem { product: String::from("gadget"), unit_price_cents: 9_900, quantity: 1 },
],
discount_percent: 10,
};
println!("Order #{} for {}", order.id, order.customer);
for item in &order.items {
println!(" {:<8} {:>2} x {:>6.2}", item.product, item.quantity, item.unit_price_cents as f64 / 100.0);
}
println!(" subtotal {:.2}", subtotal_cents(&order) as f64 / 100.0);
println!(" total {:.2}", total_cents(&order) as f64 / 100.0);
}Order #5012 for acme
widget 4 x 12.50
gadget 1 x 99.00
subtotal 149.00
total 134.1026. Realistic example: an employee directory
Searching returns references to the matching employees, so nothing is copied.
#[derive(Debug)]
struct Employee {
id: u32,
name: String,
department: String,
salary: u32,
}
fn in_department<'a>(staff: &'a [Employee], department: &str) -> Vec<&'a Employee> {
let mut result = Vec::new();
for e in staff {
if e.department == department {
result.push(e);
}
}
result
}
fn payroll(staff: &[Employee]) -> u32 {
let mut total = 0;
for e in staff {
total += e.salary;
}
total
}
fn main() {
let staff = vec![
Employee { id: 1, name: String::from("Ana"), department: String::from("Engineering"), salary: 9_000 },
Employee { id: 2, name: String::from("Ben"), department: String::from("Sales"), salary: 6_500 },
Employee { id: 3, name: String::from("Cy"), department: String::from("Engineering"), salary: 8_200 },
];
for e in in_department(&staff, "Engineering") {
println!("#{} {}", e.id, e.name);
}
println!("monthly payroll: {}", payroll(&staff));
}#1 Ana
#3 Cy
monthly payroll: 23700The 'a annotations say "the returned references point into staff". The compiler requires them here because there are two reference parameters; Lifetimes explains why.
27. Realistic example: geometry with small Copy structs
#[derive(Debug, Clone, Copy)]
struct Point {
x: f64,
y: f64,
}
#[derive(Debug, Clone, Copy)]
struct Rect {
top_left: Point,
width: f64,
height: f64,
}
fn contains(r: Rect, p: Point) -> bool {
p.x >= r.top_left.x && p.x <= r.top_left.x + r.width
&& p.y >= r.top_left.y && p.y <= r.top_left.y + r.height
}
fn overlaps(a: Rect, b: Rect) -> bool {
a.top_left.x < b.top_left.x + b.width && b.top_left.x < a.top_left.x + a.width
&& a.top_left.y < b.top_left.y + b.height && b.top_left.y < a.top_left.y + a.height
}
fn main() {
let window = Rect { top_left: Point { x: 0.0, y: 0.0 }, width: 800.0, height: 600.0 };
let button = Rect { top_left: Point { x: 700.0, y: 550.0 }, width: 120.0, height: 40.0 };
let click = Point { x: 750.0, y: 560.0 };
println!("click in button: {}", contains(button, click));
println!("button overlaps window: {}", overlaps(window, button));
println!("button fully inside: {}", contains(window, Point { x: 820.0, y: 590.0 }));
}click in button: true
button overlaps window: true
button fully inside: false28. Realistic example: updating a user profile
A form submission changes some fields and leaves the rest. The function borrows the profile mutably and only touches what the update contains.
#[derive(Debug, Clone, Default)]
struct Profile {
display_name: String,
bio: String,
website: String,
newsletter: bool,
}
#[derive(Debug, Default)]
struct ProfileUpdate {
display_name: String, // empty means "unchanged"
website: String,
newsletter: bool,
set_newsletter: bool,
}
fn apply(profile: &mut Profile, update: &ProfileUpdate) -> u32 {
let mut changed = 0;
if !update.display_name.is_empty() && update.display_name != profile.display_name {
profile.display_name = update.display_name.clone();
changed += 1;
}
if !update.website.is_empty() {
profile.website = update.website.clone();
changed += 1;
}
if update.set_newsletter && update.newsletter != profile.newsletter {
profile.newsletter = update.newsletter;
changed += 1;
}
changed
}
fn main() {
let mut profile = Profile {
display_name: String::from("ferris"),
bio: String::from("Rustacean"),
..Default::default()
};
let before = profile.clone();
let update = ProfileUpdate {
website: String::from("https://rust-lang.org"),
newsletter: true,
set_newsletter: true,
..Default::default()
};
let changed = apply(&mut profile, &update);
println!("{changed} field(s) changed");
println!("before: {:?}", before);
println!("after: {:?}", profile);
}2 field(s) changed
before: Profile { display_name: "ferris", bio: "Rustacean", website: "", newsletter: false }
after: Profile { display_name: "ferris", bio: "Rustacean", website: "https://rust-lang.org", newsletter: true }The "empty string means unchanged" convention is fragile. Option gives a much better way to say "this field may be absent": Option<String>.
Summary
struct Name { field: Type, ... }defines a type with named fields. Every field must be set when creating an instance.- Tuple structs (
struct Meters(f64)) have unnamed fields. One-field tuple structs, newtypes, make distinct types at no cost. - Unit structs (
struct Marker;) have no fields. - Mutability belongs to the variable, not to individual fields.
- A struct owns its fields. Moving, borrowing, partial moves, and destructuring work just like tuples.
#[derive(Debug, Clone, Copy, PartialEq, Default)]generates common behavior.- Structs have no hidden header, and are stored inline.
Next: Methods & Associated Functions, where structs get behavior.
