11. Structs (named, tuple, unit)
Structs are the most basic user-defined type — they group values under meaningful names. Rust has three flavors: named structs with field names, tuple structs with positional fields, and unit structs with no data. Methods live in separate `impl` blocks.
What you'll learn
- 1Define named / tuple / unit structs
- 2Write methods and associated functions in an impl block
- 3Use struct update syntax (`..base`) to partially copy a struct
- 4Enable debug printing with `#[derive(Debug)]`
- 5Distinguish &self / &mut self / self receivers
Overview
You could use `(f64, f64)` for a point — but `.0` `.1` accessors hide intent. A struct gives those same fields **meaningful names**, boosting self-documenting power.
Core Concepts
1) The three forms
// named — has field names
struct Point { x: f64, y: f64 }
// tuple struct — positional, no names
struct Color(u8, u8, u8);
// unit struct — no data, marker
struct Marker;2) impl — methods and associated functions
- **Method** — first parameter is `self`, `&self`, or `&mut self`. Called on an instance
- **Associated function** — no self. Called as `Type::func()`. Often used as constructors
3) self in three shapes
| Receiver | Meaning | Use case |
|---|---|---|
| &self | shared reference | read methods |
| &mut self | exclusive reference | write methods |
| self | takes ownership | transform / consume methods |
4) Struct update syntax
`..base` builds a fresh instance by partially overriding fields from another.
Hands-on Examples
#[derive(Debug, Clone)]
struct Point { x: f64, y: f64 }
impl Point {
// associated function — constructor
fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
// method — &self read
fn distance_from_origin(&self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}
// method — &mut self write
fn translate(&mut self, dx: f64, dy: f64) {
self.x += dx;
self.y += dy;
}
}
fn main() {
let mut p = Point::new(3.0, 4.0);
println!("{}", p.distance_from_origin()); // 5.0
p.translate(1.0, 1.0);
println!("{:?}", p); // Point { x: 4.0, y: 5.0 }
}Tuple struct as a simple wrapper:
struct Color(u8, u8, u8);
fn main() {
let red = Color(255, 0, 0);
println!("R={}", red.0);
}Struct update syntax:
#[derive(Debug)]
struct User { name: String, email: String, active: bool }
fn main() {
let u1 = User { name: "Alice".into(), email: "a@b.c".into(), active: true };
let u2 = User { email: "new@b.c".into(), ..u1 }; // name, active reused
println!("{:?}", u2);
}Common Mistakes
Q. println!("{}", p) doesn't work
A. Structs don't implement Display by default. Derive Debug and print with `{:?}` / `{:#?}`, or write `impl fmt::Display` yourself.
Q. &self vs self — which one?
A. **Read = &self, write = &mut self, transform/consume = self**. This three-way split is one of the cleanest aspects of Rust.
Q. Tuple struct vs. a plain tuple?
A. A tuple struct is a **named distinct type**. `Meters(f64)` and `Feet(f64)` won't accidentally interconvert — the compiler stops unit-mix-ups.
Recap
- Three forms: named / tuple / unit
- Methods and associated functions live in impl
- Three self receivers: &self / &mut self / self
- `..base` syntax creates partially-updated instances
Try It Yourself
- Write a Rectangle struct with area() and perimeter() methods
- Add a User::update_email(&mut self, new: String) method and call it
- Build tuple structs Meters(f64) / Feet(f64) and write a conversion function — observe type safety
All lecture materials and example code are openly available on GitHub.
View on GitHub ↗