Structs

Bundle related data into your own named types — plus the one thing about struct update syntax that quietly catches almost everyone off guard the first time they use it.

12 min read4 learning objectives

What You'll Learn

  • Define a struct and create instances of it
  • Use field-init shorthand and struct update syntax — and know exactly what each one does to ownership
  • Tell named-field structs, tuple structs, and unit-like structs apart, and know when to reach for each
  • Make any struct printable for debugging with #[derive(Debug)]

You've been reaching for tuples whenever you needed to group a couple of related values — and that's worked fine, for pairs. The moment you've got four or five related pieces of data, though, tuples stop being convenient and start being a liability.

When tuples stop being enough

You'd have to scroll back up to the definition to answer that — and so would everyone who reads this code after you, including future-you, six months from now, at 11pm, trying to fix an unrelated bug. Positional access (.0, .1, .2...) is fine for two values whose order is obvious. It quietly turns hostile the moment "related" starts meaning "five different things with five different meanings."

A struct fixes this by letting you put a name on every single piece.

Defining and building a struct

struct User { ... } defines a brand-new type — a named bundle of named fields, each with its own type. Once the definition exists, you build an instance by naming the struct and supplying a value for every field, in whatever order you like (Rust matches by name, not position, so order genuinely doesn't matter here). Reach into a field with a dot, exactly like a tuple — except now the name tells you, and your editor's autocomplete, precisely what you're holding.

Mutating an instance

There's a wrinkle worth knowing up front: Rust has no concept of "this one field is mutable, the rest are locked." Mutability belongs to the entire binding. Mark user1 as mut, and every field becomes assignable; leave the mut off, and none of them are. It might look like an all-or-nothing trade-off — and it is, deliberately — because it keeps "can this change?" a question you can answer just by glancing at how something was declared, instead of hunting down every function that might reach into one specific field.

Field-init shorthand

Constructor-style functions constantly end up with parameter names that match field names exactly. When that happens, you're allowed to drop the repetition:

That's a small thing, but small things like this add up across a codebase — real-world Rust is full of little shorthands that trim away exactly the kind of repetition the compiler could already see was redundant.

Struct update syntax — and a pop quiz from the ownership lesson

Building a near-copy of an existing instance, with one or two fields changed, is common enough that Rust gives it dedicated syntax:

..user1 means "and fill in everything else from user1." Tidy. But here's a pop quiz straight out of the ownership lesson: once user2 exists, is user1 still usable as a whole?

It depends entirely on which fields moved versus which ones copied — and ..user1 doesn't clone user1, it takes its remaining values directly. active and sign_in_count are a bool and a u64 — both Copy — so those get duplicated harmlessly. But username is a String, which is not Copy, and it gets moved into user2. The instant that happens, user1 becomes partially moved-from, and the compiler refuses to let you use it as a whole value again — though, interestingly, you could still read user1.email on its own, since that specific field never moved. The borrow checker tracks individual fields, not just entire variables. If that paragraph made complete sense on the first read, ownership has well and truly clicked for you.

Tuple structs: a name on top of a tuple's shape

Sometimes you want a brand-new, distinct type, but naming each field would just add ceremony without adding clarity. Tuple structs split the difference — a name for the type, positional fields underneath, just like a regular tuple:

Point and Color have identical internal layouts — three i32s apiece — and yet you cannot accidentally pass a Color where a Point is expected. To the compiler, they're as different as String and u32. That's the entire point of giving them names: the name carries meaning that the shape alone never could.

There's a third, even sparser flavor worth knowing exists: the unit-like struct, written struct AlwaysEqual; — no fields whatsoever. They're rare, but they earn their keep when you need a type to exist purely so you can implement a trait on it. We'll meet that pattern for real in the traits lesson.

Printing a struct: #[derive(Debug)]

Try writing println!("{rect}") for a struct, and the compiler will (politely) refuse — your struct has no idea how to format itself for people to read, because nobody ever taught it how. Writing that formatting code by hand for every struct you ever define would be miserable, so Rust lets you ask for a sensible default with one line above the definition:

#[derive(Debug)] is an attribute — a small instruction to the compiler, written #[...] — and this one says, roughly, "please generate a debug-printing implementation for this type for me." {rect:?} then prints the whole struct on one line, and {rect:#?} ("pretty-print") spreads it across several, indented lines — genuinely useful the moment your structs grow past two or three fields. Get comfortable with both. You'll reach for them constantly once you start debugging real programs.

Quick exercise

Define struct Movie with fields for title (String), release_year (u32), and rating (f32). Write a function that builds one from plain parameters using field-init shorthand, derive Debug on the struct, and pretty-print an instance with {:#?}.

Right now, that Rectangle from the example above is just a box holding two numbers — it can't actually do anything yet. That changes immediately: methods are how you teach a struct to behave like the thing it represents, and writing your first one is one of the more satisfying small moments in learning this language.