Why this matters
Real data comes in bundles: a user has a name, an email and an ID; a rectangle has a width and a height. Tuples can group values, but (String, String, u64) says nothing about which field is which, and mixing up positions is an easy bug. A struct gives the bundle a name and each field a name and type. Structs are the main way you model your problem in Rust, and with methods (next lesson) they become the building blocks of every program.
Defining and creating a struct
#[derive(Debug)]
struct User {
name: String,
email: String,
id: u64,
active: bool,
}
fn main() {
let user = User {
email: String::from("ada@example.com"),
name: String::from("Ada"),
id: 1,
active: true,
};
println!("{} <{}> id={}", user.name, user.email, user.id);
println!("{:?}", user);
println!("{:#?}", user);
}
Ada <ada@example.com> id=1
User { name: "Ada", email: "ada@example.com", id: 1, active: true }
User {
name: "Ada",
email: "ada@example.com",
id: 1,
active: true,
}
struct Name { field: Type, ... }defines a new type. Types are named inCamelCase, fields insnake_case.- A value is created by naming every field (in any order): a struct literal.
user.namereads a field.#[derive(Debug)]is an attribute asking the compiler to generate the code for{:?}printing. Without it,println!("{:?}", user)is an error. Module 10 explainsderive.
Ownership of fields
A struct owns its fields. Above, user owns two Strings; when user is dropped, they are dropped (fields in declaration order). Storing owned types like String (not &str) in structs is the norm: the struct is then self-contained and can be passed around freely. Structs holding references need lifetime annotations, which come in module 10.
Moving a field out of a struct moves just that field (a "partial move"); the rest of the struct can still be used, but the struct as a whole cannot:
#[derive(Debug)]
struct User {
name: String,
id: u64,
}
fn main() {
let u = User { name: String::from("Ada"), id: 7 };
let n = u.name;
println!("{n} {}", u.id);
println!("{:?}", u);
}
error[E0382]: borrow of partially moved value: `u`
--> main.rs:11:22
|
9 | let n = u.name;
| ------ value partially moved here
10 | println!("{n} {}", u.id);
11 | println!("{:?}", u);
| ^ value borrowed here after partial move
|
= note: partial move occurs because `u.name` has type `String`, which does not implement the `Copy` trait
Borrow the field instead (let n = &u.name;) if you need the struct intact.
Mutability
Mutability belongs to the binding, not to individual fields. To change any field, the whole variable must be mut:
struct Counter {
label: String,
count: u32,
}
fn main() {
let mut c = Counter { label: String::from("clicks"), count: 0 };
c.count += 1;
c.count += 1;
c.label.push_str(" (total)");
println!("{}: {}", c.label, c.count);
}
clicks (total): 2
There is no way to mark a single field as mutable or immutable; you control access with privacy instead (module 12).
Shorthands
Field init shorthand: when a variable has the same name as a field, write it once:
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn make_point(x: i32, y: i32) -> Point {
Point { x, y }
}
fn main() {
let p = make_point(3, 4);
let q = Point { y: 10, ..p };
println!("{:?} {:?}", p, q);
}
Point { x: 3, y: 4 } Point { x: 3, y: 10 }
Struct update syntax: ..p fills in all fields not listed from p. Careful: it moves those fields, so if they include a String, p can no longer be used as a whole afterwards (for Copy fields like these, p stays usable).
Tuple structs and unit structs
A tuple struct has a name but unnamed fields, accessed by position:
struct Rgb(u8, u8, u8);
struct Meters(f64);
struct Seconds(f64);
fn speed(d: Meters, t: Seconds) -> f64 {
d.0 / t.0
}
fn main() {
let orange = Rgb(255, 165, 0);
println!("#{:02x}{:02x}{:02x}", orange.0, orange.1, orange.2);
println!("{} m/s", speed(Meters(100.0), Seconds(9.58)));
}
#ffa500
10.438413361169102 m/s
A one-field tuple struct is called a newtype. Meters and Seconds both hold an f64, but they are different types, so speed(Seconds(9.58), Meters(100.0)) is a compile error. Newtypes cost nothing at run time (same size and layout as the inner value) and prevent mix-ups like the unit confusion that famously destroyed the Mars Climate Orbiter.
A unit struct has no fields at all: struct Marker;. It takes zero bytes and is useful with traits (module 10).
Memory layout
A struct is laid out like a tuple: its fields placed in one block, with padding for alignment, and Rust is free to reorder fields to reduce padding:
#[allow(dead_code)]
struct User {
active: bool,
id: u64,
name: String,
}
fn main() {
println!("{}", std::mem::size_of::<User>());
}
40
User (40 bytes, one possible layout)
+--------------------------------+--------+------+---------+
| name: ptr | len | cap (24) | id (8) | act | padding |
+--------------------------------+--------+------+---------+
|
v
heap: name's bytes (not part of the struct's size)
A struct is a single value of fixed size. A local User lives entirely in the stack frame (with its strings' contents on the heap); a Vec<User> stores the 40-byte structs back to back in its buffer. No hidden object headers, no pointers between the fields: what you declare is what you get.
Common mistakes
Missing fields in a literal:
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 1 };
}
error[E0063]: missing field `y` in initializer of `Point`
--> main.rs:7:13
|
7 | let p = Point { x: 1 };
| ^^^^^ missing `y`
Every field must be given (there are no implicit defaults; module 10's Default trait provides an explicit way).
Printing without #[derive(Debug)]: E0277 "Point doesn't implement Debug", with a help suggesting the derive.
Assigning to a field of an immutable binding: E0594 "cannot assign to p.points, as p is not declared as mutable".
Using &str fields too early. struct User { name: &str } fails with E0106 (missing lifetime). Use String until module 10.
Recap
struct Name { field: Type }defines a type; create withName { field: value }, read withvalue.field.- A struct owns its fields; moving a field out partially moves the struct.
- Mutability is per binding (
let mut), covering all fields. - Field init shorthand
Point { x, y }and update syntax..othersave typing. - Tuple structs (
struct Meters(f64)) and newtypes give distinct types to the same representation. - A struct is one fixed-size block of its fields plus padding;
#[derive(Debug)]enables{:?}.
Going further: print size_of for a struct of (u8, u32, u8) fields and for the same with #[repr(C)] above it, which forbids reordering (used for C interop, module 15). Explain the difference.