Structs
Define and instantiate structs, use struct update syntax and tuple structs, and attach behavior with methods and associated functions in impl blocks.
Introduction
Structs let you group related data together under one custom type, and give it behavior through methods. If you've used classes in other languages, structs plus `impl` blocks fill a similar role in Rust — minus inheritance.
This lesson covers defining and instantiating structs, tuple structs, struct update syntax, and how methods and associated functions work with ownership and borrowing.
- How to define and instantiate a struct.
- Struct update syntax for creating a new instance from an existing one.
- Tuple structs and unit-like structs.
- How to attach methods and associated functions with impl blocks.
- How #[derive(Debug)] lets you print a struct for debugging.
Defining and Instantiating Structs
A struct is defined with the `struct` keyword, listing named fields and their types. To create an instance, provide a value for every field. If a variable name matches a field name exactly, Rust lets you use field init shorthand and just write the name once.
Struct Update Syntax
You can create a new struct instance from most of an existing instance's values using `..` struct update syntax — for example `let rect2 = Rectangle { width: 10, ..rect1 };` reuses every field from `rect1` except `width`. Be aware that this follows ownership rules like any other assignment: if any reused field is not `Copy`, that field is moved out of the original instance.
Tuple Structs and Unit-Like Structs
A tuple struct, like `struct Point(i32, i32);`, has fields without names — you access them positionally with `.0`, `.1`, and so on. It's useful when the field names would add no clarity and the type itself is the important part. A unit-like struct, like `struct Marker;`, has no fields at all — it's mainly useful as a lightweight marker type, often paired with traits.
Methods with impl Blocks
Methods are functions defined inside an `impl` block for a specific struct. Every method takes some form of `self` as its first parameter, and the form you choose matters a great deal because of ownership rules.
| Signature | Meaning |
|---|---|
| fn method(&self) | Borrows the instance immutably — can read fields but not modify them. |
| fn method(&mut self) | Borrows the instance mutably — can read and modify fields. |
| fn method(self) | Takes ownership of the instance — the original value can no longer be used afterward. |
| fn new(...) -> Self | An associated function with no self parameter — called as Type::new(...), commonly used as a constructor. |
Associated Functions
Functions defined inside an `impl` block that don't take `self` at all are called associated functions rather than methods. The most common example is a constructor, conventionally named `new`, called with `Type::new(...)` rather than dot syntax. `Self` inside an `impl` block is shorthand for the type being implemented.
Debug Formatting
By default, structs cannot be printed with `println!("{}", ...)` or even the debug formatter `{:?}` — Rust requires you to opt in. Adding `#[derive(Debug)]` above a struct definition automatically generates a `Debug` implementation, letting you print the whole struct with `{:?}` (or `{:#?}` for a pretty-printed, multi-line version) for quick debugging.
Code Example
#[derive(Debug)]struct Rectangle { width: u32, height: u32,}
impl Rectangle { // Associated function (constructor-style, no self) fn new(width: u32, height: u32) -> Self { Self { width, height } }
// Method that borrows self immutably fn area(&self) -> u32 { self.width * self.height }}
fn main() { let rect = Rectangle::new(30, 50); println!("{:?}", rect); println!("Area: {}", rect.area());}Click Run to see what this code prints.
Common Mistakes
- Trying to print a struct with `{:?}` without adding `#[derive(Debug)]` first — this is a compile error, not a runtime one.
- Using `self` instead of `&self` on a method that doesn't need to consume the struct, which moves the instance and makes it unusable afterward.
- Confusing tuple struct field access — remember it's `.0`, `.1`, not named fields.
- Forgetting that struct update syntax (`..other`) moves any non-Copy fields out of `other`, making it partially invalid afterward.
Best Practices
- Default to `&self` for methods, reaching for `&mut self` only when you truly need to mutate, and `self` only when consuming the value is intentional.
- Add `#[derive(Debug)]` to nearly every struct you write — the ability to quickly print a value for debugging is worth almost nothing in cost.
- Use `Self` inside impl blocks instead of repeating the struct's name, so renaming the type later requires fewer edits.
- Prefer named-field structs over tuple structs unless the values genuinely have no useful names or you specifically want a lightweight wrapper type.
Frequently Asked Questions
No. Rust deliberately has no struct inheritance; shared behavior is achieved through traits and composition instead, which you'll see in more advanced material.
A method takes some form of self and is called with dot syntax (instance.method()); an associated function has no self parameter and is called with Type::function().
Self is shorthand for whatever type the impl block is for — it means the same thing as writing the struct name, but stays correct automatically if the struct is ever renamed.
Yes — a struct can have several impl blocks, which is commonly used to separate, for example, trait implementations from the struct's own methods.
Key Takeaways
- Structs group related, named fields into a single custom type.
- Tuple structs use positional `.0`/`.1` access; unit-like structs have no fields.
- Methods live in impl blocks and take &self, &mut self, or self depending on whether they read, mutate, or consume the instance.
- Associated functions (no self) are commonly used as constructors, called via Type::new(...).
- #[derive(Debug)] enables printing a struct with {:?} for quick debugging.
Summary
You now know how to model your own data with structs and attach behavior to it with methods, all while respecting ownership and borrowing rules. Next, you'll learn enums — Rust's powerful way to represent a value that can be one of several different shapes — and the match expression used to handle them.