Guide contents

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

Rust
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);
}
Output
ferris <ferris@example.com> age 8 active true

Fields 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

Rust✗ does not compile · E0063
struct User {
    username: String,
    email: String,
    age: u32,
}

fn main() {
    let user = User {
        username: String::from("ferris"),
        age: 8,
    };
    println!("{}", user.username);
}
Output
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.

Rust
#[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);
}
Output
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

Rust✗ does not compile · E0277
struct Product {
    sku: String,
}

fn main() {
    let p = Product { sku: String::from("KB-001") };
    println!("{:?}", p);
}
Output
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.

Rust
#[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);
}
Output
Counter { name: "page_views_total", count: 2 }

Changing a field of an immutable instance is an error:

Rust✗ does not compile · E0594
struct Counter {
    count: u64,
}

fn main() {
    let c = Counter { count: 0 };
    c.count += 1;
    println!("{}", c.count);
}
Output
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.

Rust
#[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);
}
Output
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...".

Rust
#[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);
}
Output
Settings { theme: "dark", font_size: 16, autosave: true, language: "en" }
defaults font size is still readable: 14

8. 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.

Rust✗ does not compile · E0382
#[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);
}
Output
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` trait

Fields 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.

Rust
#[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);
}
Output
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.

Rust✗ does not compile · E0308
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);
}
Output
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:

Rust
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));
}
Output
altitude set to 10668.0 m

11. Newtypes for IDs

Wrapping IDs stops you from passing an order ID where a customer ID belongs.

Rust
#[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));
}
Output
customer 42 cancels order 9001
CustomerId(42) == CustomerId(42): true

Clone, 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.

Rust
#[derive(Debug)]
struct Heartbeat;

fn main() {
    let h = Heartbeat;
    println!("{:?} takes {} bytes", h, std::mem::size_of::<Heartbeat>());
}
Output
Heartbeat takes 0 bytes

13. Nested structs

Rust
#[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);
}
Output
Homer ships to 1 Main St, Springfield
Homer bills to Capital City 67890

A 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.

Rust✗ does not compile · E0382
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);
}
Output
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.

Rust
#[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));
}
Output
Ana: 125.50
Ana: 120.50

Field 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.

Rust
#[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);
}
Output
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.

Rust
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]);
}
Output
photo.jpg (3 bytes)
first byte: 0xFF

18. Destructuring a struct

A let can take a struct apart into variables, like a tuple. .. ignores the remaining fields.

Rust
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}");
}
Output
POST /api/orders
timeout 100

19. Destructuring a borrowed struct

Destructuring a reference gives references to the fields, so nothing is moved.

Rust
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);
}
Output
x=-3 y=4, distance=7
p is still usable: (-3, 4)

20. A Vec of structs

Rust
#[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");
}
Output
[x] design schema
[x] write migrations
[ ] load test
remaining: 5 hours

21. 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.

Rust
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);
}
Output
Pixel:   4 bytes
Reading: 16 bytes
two pixels: 8 bytes
first pixel is red: true
second pixel is half transparent: true

A 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.

Rust
#[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
}
Output
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.

Rust
#[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);
}
Output
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".

Rust
#[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);
}
Output
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

Rust
#[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);
}
Output
Order #5012 for acme
  widget    4 x  12.50
  gadget    1 x  99.00
  subtotal 149.00
  total    134.10

26. Realistic example: an employee directory

Searching returns references to the matching employees, so nothing is copied.

Rust
#[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));
}
Output
#1 Ana
#3 Cy
monthly payroll: 23700

The '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

Rust
#[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 }));
}
Output
click in button: true
button overlaps window: true
button fully inside: false

28. 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.

Rust
#[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);
}
Output
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.