11 — Structs
Structs group related data. Rust structs come in three flavors.
Named-Field Structs
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
let u = User {
username: String::from("ada"),
email: String::from("ada@example.com"),
sign_in_count: 1,
active: true,
};
Field-Init Shorthand
fn new(email: String, username: String) -> User {
User { email, username, active: true, sign_in_count: 0 }
}
When a variable name matches the field, omit : value.
Struct Update Syntax
let u2 = User { email: String::from("ada2@x.com"), ..u };
..ucopies/moves the remaining fields fromu.- Like a partial move —
u.usernameis now invalid ifStringwas moved (non-Copy). - For
Copyfields, they're copied; for non-Copy, they're moved out ofu.
Tuple Structs
struct Color(i32, i32, i32);
let c = Color(255, 128, 0);
let r = c.0;
- Look like tuples but are distinct types.
- Useful for newtype pattern:
struct Meters(f64);prevents mixing with otherf64. - Pattern match:
let Color(r, g, b) = c;.
Unit Structs
struct AlwaysEqual;
let _a = AlwaysEqual;
Zero-sized; useful for trait implementations with no data (e.g., marker traits, type-state).
impl Blocks
impl User {
fn new(email: String, username: String) -> Self {
User { email, username, active: true, sign_in_count: 0 }
}
fn is_active(&self) -> bool { self.active }
fn sign_in(&mut self) { self.sign_in_count += 1; }
fn deactivate(self) -> User { User { active: false, ..self } }
}
You can split impl across multiple blocks (common in real codebases: one for methods, one for trait impls).
Methods vs Associated Functions
- Methods take
&self/&mut self/selfand are called on instances:u.is_active(). - Associated functions (no
self) are constructors:User::new(...). - Convention:
newfor the canonical constructor,with_xfor variant constructors.
Lifetime on Structs (recap)
struct Excerpt<'a> { part: &'a str }
impl<'a> Excerpt<'a> { fn part(&self) -> &'a str { self.part } }
Generic Structs
struct Point<T> { x: T, y: T }
impl<T> Point<T> {
fn x(&self) -> &T { &self.x }
}
impl Point<f64> { // specialized impl for f64
fn distance(&self, other: &Self) -> f64 {
((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
}
}
Type params can be specialized: methods exist only for a specific T.
Constants in Structs
struct Grid<const W: usize, const H: usize> {
cells: [[u8; W]; H],
}
let g: Grid<10, 20> = Grid { cells: [[0; 10]; 20] };
Const generics (1.51+) allow parametrizing by compile-time constants. Limited to integers/bool/char for now (full generic constants are unstable).
Derive Macros
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
struct Pos { x: i32, y: i32 }
Common derives:
Debug→{:?}Clone, Copy→ value duplicationPartialEq, Eq→==PartialOrd, Ord→ comparison and sortingHash→ usable inHashSet/HashMapDefault→Pos::default()
Eq/Ord require no NaN-like values — floats only get PartialEq/PartialOrd.
Default
#[derive(Default)]
struct Config { host: String, port: u16 }
let c = Config { host: "localhost".into(), ..Default::default() };
Idiomatic way to provide "default with overrides".
Debug vs Display
Debugis derived, machine-readable-ish ({:?}/ pretty{:#?}).Displayis user-facing; you must write it manually.
impl std::fmt::Display for User {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{} <{}>", self.username, self.email)
}
}
Struct Updates and Moves
let u = User { /* filled */ };
let email = u.email; // partial move
// u is partially moved; can still read other fields, but not u as a whole
Reconstruct with .. if needed.
Edge Cases & Pitfalls
- Out-of-order field initialization is allowed — order doesn't matter in struct literals.
- Mutability is per-binding, not per-field: there's no
mutfield modifier. UseCell/RefCellfor interior mutability of single fields. - No inheritance: Rust has no class inheritance. Use composition + traits.
- Private fields: by default, fields are private to the module. Use
pubto expose. pub(crate): visible within the same crate only.#[non_exhaustive]prevents external crates from constructing the struct with literal syntax — forces them to use a constructor (future-proofing).- Self-referential structs: not expressible directly in safe Rust (the borrow checker can't describe the relationship); use crates like
ouroborosor own the data. - ZST struct:
struct Marker;has size 0. - Field order and
Drop: struct fields drop in declaration order (RFC 1857), unlike locals which drop in reverse order. This can matter for field destructors that depend on each other.
impl Method Dispatch
- Methods taking
selfby value consume the receiver. - Method resolution finds methods on
Self,&Self,&mut Selfautomatically based on call syntax. - Auto-ref/deref lets you call
&selfmethods on owned values and vice versa.
Memory Layout
- Reorder fields for minimal padding — the compiler does this by default (repr optimization). Use
#[repr(C)]to force C-compatible layout (FFI). Use#[repr(transparent)]for newtype wrappers (same layout as inner). Use#[repr(packed)]to disable padding (careful with alignment → unaligned reads are UB).
Summary
Structs come in named, tuple, and unit forms. Methods live in impl blocks. Derive macros give you common traits for free. Const generics, generics, and lifetimes parametrize them. Memory layout can be controlled with repr attributes.
Next: Enums — Rust's algebraic data types.