Reading — step 1 of 7
Learn
Structs are how Rust groups related data into a named type. Chapter 5 of The Rust Programming Language book makes them the gateway from procedural code to type-driven design — and rightly so. Once you start defining your own types, the compiler can catch a whole new class of mistakes.
The three flavors of structs
1. Classic named-field structs
struct User {
name: String,
age: u32,
active: bool,
}
let u = User {
name: String::from("Ada"),
age: 36,
active: true,
};
println!("{}", u.name); // Ada
Create with the field-init syntax. Access fields with .. Like a struct in C or a record in most languages.
2. Tuple structs — named tuples
When the field names don't add value:
struct Point(i32, i32);
struct Color(u8, u8, u8);
let origin = Point(0, 0);
let red = Color(255, 0, 0);
println!("{} {}", origin.0, origin.1); // access by position
Useful when the type itself names the meaning (Point, Color) but individual fields don't need names. Different tuple structs are DIFFERENT TYPES — Point and Color aren't interchangeable even though both are (i32, i32)-shaped.
3. Unit structs — no fields
struct AlwaysReady;
let ready = AlwaysReady;
Useful for marker types and trait conformance without state.
Methods with impl blocks
Methods attach to a struct via impl:
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn is_square(&self) -> bool {
self.width == self.height
}
// Associated function (no self) — like a static constructor
fn square(side: u32) -> Rectangle {
Rectangle { width: side, height: side }
}
}
let r = Rectangle { width: 3, height: 4 };
println!("{}", r.area()); // 12
let sq = Rectangle::square(5); // associated function syntax
println!("{}", sq.area()); // 25
The first parameter is the receiver:
&self— borrow immutably (read-only access)&mut self— borrow mutably (modify the struct)self— take ownership (consume; rare)
Updating with the struct update syntax
let u1 = User { name: String::from("Ada"), age: 36, active: true };
let u2 = User {
name: String::from("Linus"),
..u1 // copy remaining fields from u1
};
The ..u1 says "and the other fields come from u1." Note: this MOVES non-Copy fields out of u1, so u1 may not be fully usable afterward.
Derive macros — automatic trait implementations
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
let p1 = Point { x: 3, y: 4 };
let p2 = p1.clone();
println!("{:?}", p1); // Debug — prints Point { x: 3, y: 4 }
println!("{}", p1 == p2); // PartialEq — prints true
With one line, you get useful trait implementations the compiler generates from the field types. Almost every struct in real Rust code has at least #[derive(Debug)] for printing.
Common mistakes
- Forgetting
&selfon methods — without a receiver, it's an associated function (called viaType::method), not an instance method. - Using
pub structwithoutpubon fields — the struct is public but its fields are private. Often desired for encapsulation. - Cloning when borrowing would do — clone has cost. Use
&selfborrowing first, only clone when ownership is needed. - Forgetting to derive Debug — then
println!("{:?}", ...)won't compile. Most types should derive Debug for diagnostics.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…