Structs, Enums and Pattern Matching · about 40 min

Structs: Grouping Data

Why this matters

Real data comes in bundles: a user has a name, an email and an ID; a rectangle has a width and a height. Tuples can group values, but (String, String, u64) says nothing about which field is which, and mixing up positions is an easy bug. A struct gives the bundle a name and each field a name and type. Structs are the main way you model your problem in Rust, and with methods (next lesson) they become the building blocks of every program.

Defining and creating a struct

#[derive(Debug)]
struct User {
    name: String,
    email: String,
    id: u64,
    active: bool,
}

fn main() {
    let user = User {
        email: String::from("ada@example.com"),
        name: String::from("Ada"),
        id: 1,
        active: true,
    };
    println!("{} <{}> id={}", user.name, user.email, user.id);
    println!("{:?}", user);
    println!("{:#?}", user);
}
Ada <ada@example.com> id=1
User { name: "Ada", email: "ada@example.com", id: 1, active: true }
User {
    name: "Ada",
    email: "ada@example.com",
    id: 1,
    active: true,
}

Ownership of fields

A struct owns its fields. Above, user owns two Strings; when user is dropped, they are dropped (fields in declaration order). Storing owned types like String (not &str) in structs is the norm: the struct is then self-contained and can be passed around freely. Structs holding references need lifetime annotations, which come in module 10.

Moving a field out of a struct moves just that field (a "partial move"); the rest of the struct can still be used, but the struct as a whole cannot:

#[derive(Debug)]
struct User {
    name: String,
    id: u64,
}

fn main() {
    let u = User { name: String::from("Ada"), id: 7 };
    let n = u.name;
    println!("{n} {}", u.id);
    println!("{:?}", u);
}
error[E0382]: borrow of partially moved value: `u`
  --> main.rs:11:22
   |
9  |     let n = u.name;
   |             ------ value partially moved here
10 |     println!("{n} {}", u.id);
11 |     println!("{:?}", u);
   |                      ^ value borrowed here after partial move
   |
   = note: partial move occurs because `u.name` has type `String`, which does not implement the `Copy` trait

Borrow the field instead (let n = &u.name;) if you need the struct intact.

Mutability

Mutability belongs to the binding, not to individual fields. To change any field, the whole variable must be mut:

struct Counter {
    label: String,
    count: u32,
}

fn main() {
    let mut c = Counter { label: String::from("clicks"), count: 0 };
    c.count += 1;
    c.count += 1;
    c.label.push_str(" (total)");
    println!("{}: {}", c.label, c.count);
}
clicks (total): 2

There is no way to mark a single field as mutable or immutable; you control access with privacy instead (module 12).

Shorthands

Field init shorthand: when a variable has the same name as a field, write it once:

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn make_point(x: i32, y: i32) -> Point {
    Point { x, y }
}

fn main() {
    let p = make_point(3, 4);
    let q = Point { y: 10, ..p };
    println!("{:?} {:?}", p, q);
}
Point { x: 3, y: 4 } Point { x: 3, y: 10 }

Struct update syntax: ..p fills in all fields not listed from p. Careful: it moves those fields, so if they include a String, p can no longer be used as a whole afterwards (for Copy fields like these, p stays usable).

Tuple structs and unit structs

A tuple struct has a name but unnamed fields, accessed by position:

struct Rgb(u8, u8, u8);
struct Meters(f64);
struct Seconds(f64);

fn speed(d: Meters, t: Seconds) -> f64 {
    d.0 / t.0
}

fn main() {
    let orange = Rgb(255, 165, 0);
    println!("#{:02x}{:02x}{:02x}", orange.0, orange.1, orange.2);
    println!("{} m/s", speed(Meters(100.0), Seconds(9.58)));
}
#ffa500
10.438413361169102 m/s

A one-field tuple struct is called a newtype. Meters and Seconds both hold an f64, but they are different types, so speed(Seconds(9.58), Meters(100.0)) is a compile error. Newtypes cost nothing at run time (same size and layout as the inner value) and prevent mix-ups like the unit confusion that famously destroyed the Mars Climate Orbiter.

A unit struct has no fields at all: struct Marker;. It takes zero bytes and is useful with traits (module 10).

Memory layout

A struct is laid out like a tuple: its fields placed in one block, with padding for alignment, and Rust is free to reorder fields to reduce padding:

#[allow(dead_code)]
struct User {
    active: bool,
    id: u64,
    name: String,
}

fn main() {
    println!("{}", std::mem::size_of::<User>());
}
40
 User (40 bytes, one possible layout)
 +--------------------------------+--------+------+---------+
 | name: ptr | len | cap (24)     | id (8) | act  | padding |
 +--------------------------------+--------+------+---------+
                 |
                 v
          heap: name's bytes (not part of the struct's size)

A struct is a single value of fixed size. A local User lives entirely in the stack frame (with its strings' contents on the heap); a Vec<User> stores the 40-byte structs back to back in its buffer. No hidden object headers, no pointers between the fields: what you declare is what you get.

Common mistakes

Missing fields in a literal:

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 1 };
}
error[E0063]: missing field `y` in initializer of `Point`
 --> main.rs:7:13
  |
7 |     let p = Point { x: 1 };
  |             ^^^^^ missing `y`

Every field must be given (there are no implicit defaults; module 10's Default trait provides an explicit way).

Printing without #[derive(Debug)]: E0277 "Point doesn't implement Debug", with a help suggesting the derive.

Assigning to a field of an immutable binding: E0594 "cannot assign to p.points, as p is not declared as mutable".

Using &str fields too early. struct User { name: &str } fails with E0106 (missing lifetime). Use String until module 10.

Recap

Going further: print size_of for a struct of (u8, u32, u8) fields and for the same with #[repr(C)] above it, which forbids reordering (used for C interop, module 15). Explain the difference.

Exercises

Exercise 1 · write code

A Rectangle

Define struct Rect { width: u32, height: u32 } with #[derive(Debug)], and a function fn area(r: &Rect) -> u32. Read two numbers (width and height, one per line) from stdin, build a Rect, and print it with {:?} followed by its area:

Rect { width: 3, height: 4 }
area: 12

File: main.rs

Starting point

use std::io;

fn read() -> u32 {
    let mut s = String::new();
    io::stdin().read_line(&mut s).unwrap();
    s.trim().parse().unwrap()
}

fn main() {
    let w = read();
    let h = read();
}

Example

input

3
4

expected output

Rect { width: 3, height: 4 }
area: 12

Example

input

10
1

expected output

Rect { width: 10, height: 1 }
area: 10
Hint 1

Put #[derive(Debug)] on the line before struct Rect.

Hint 2

Create it with Rect { width: w, height: h } and pass &r to area.

Reference solution
use std::io;

#[derive(Debug)]
struct Rect {
    width: u32,
    height: u32,
}

fn area(r: &Rect) -> u32 {
    r.width * r.height
}

fn read() -> u32 {
    let mut s = String::new();
    io::stdin().read_line(&mut s).unwrap();
    s.trim().parse().unwrap()
}

fn main() {
    let width = read();
    let height = read();
    let r = Rect { width, height };
    println!("{:?}", r);
    println!("area: {}", area(&r));
}

Exercise 2 · write code

Change a field

This program fails to compile (E0594): it tries to change a field of an immutable struct. Fix it so it prints:

Ada has 0 points
Ada has 15 points

File: main.rs

Starting point

struct Player {
    name: String,
    points: u32,
}

fn main() {
    let p = Player { name: String::from("Ada"), points: 0 };
    println!("{} has {} points", p.name, p.points);
    p.points += 15;
    println!("{} has {} points", p.name, p.points);
}

Example

expected output

Ada has 0 points
Ada has 15 points
Hint 1

Mutability belongs to the binding, and covers all fields at once: let mut p = ....

Reference solution
struct Player {
    name: String,
    points: u32,
}

fn main() {
    let mut p = Player { name: String::from("Ada"), points: 0 };
    println!("{} has {} points", p.name, p.points);
    p.points += 15;
    println!("{} has {} points", p.name, p.points);
}

Exercise 3 · write code

Newtypes prevent mix-ups

Define two tuple structs struct Meters(f64); and struct Feet(f64); and a function fn to_meters(f: Feet) -> Meters (1 foot = 0.3048 meters). Read a number of feet from stdin and print:

10 ft = 3.048 m

Print the meters value with {} (no fixed precision), reaching it with .0.

File: main.rs

Starting point

use std::io;

fn main() {
    let mut s = String::new();
    io::stdin().read_line(&mut s).unwrap();
    let ft: f64 = s.trim().parse().unwrap();
}

Example

input

10

expected output

10 ft = 3.048 m

Example

input

2.5

expected output

2.5 ft = 0.762 m
Hint 1

Create with Feet(ft), read the inner value with .0.

Reference solution
use std::io;

struct Meters(f64);
struct Feet(f64);

fn to_meters(f: Feet) -> Meters {
    Meters(f.0 * 0.3048)
}

fn main() {
    let mut s = String::new();
    io::stdin().read_line(&mut s).unwrap();
    let ft: f64 = s.trim().parse().unwrap();
    let m = to_meters(Feet(ft));
    println!("{ft} ft = {} m", m.0);
}

Exercise 4 · quiz

How big is it?

On a 64-bit machine, what is size_of::<User>() for

struct User {
    active: bool,
    id: u64,
    name: String,
}
Answer
  • 40

24 (String handle) + 8 (u64) + 1 (bool) = 33 bytes of data, rounded up to a multiple of the largest alignment (8) gives 40. Rust may reorder fields to minimize padding. The name's characters live on the heap and do not count: a struct's size is fixed at compile time.