Rust Structs, Methods and Associated Functions¶
A Rust struct groups related values under a meaningful type name. In this lesson, a fixture reading combines its device label and temperature, while methods make it clear which operations inspect, change or consume that reading.
Prerequisites and learning outcome¶
Complete references, borrowing and slices. You will define named fields, construct a value, write an impl block and choose between &self, &mut self and self. The example uses fixture data, not live temperature measurements.
Model one reading with a Rust struct¶
A tuple can hold a String and an i32, but names explain their roles more clearly. Our type uses label and millidegrees; it deliberately does not validate a sensor's operating range. A struct groups data, but that alone does not guarantee physically meaningful data or prevent every invalid operation.
Replace src/main.rs with this complete program:
Expected output:
Fields, literals and ownership¶
Reading { label, millidegrees } is field-init shorthand: it means Reading { label: label, millidegrees: millidegrees }. The String moves into its field; the i32 is Copy. Access a field through a dot, such as reading.millidegrees.
Rust has no special constructor keyword for new. It is an ordinary associated function named by convention. The literal test constructs a Reading directly. Later, module visibility can restrict construction and force callers through a validating API; our single-module example does not yet do that.
Borrowing an entire struct borrows access to its fields through that reference. Moving a non-Copy field out can leave a partially moved value: you may still access untouched fields in suitable cases, but cannot borrow the whole value as though it were intact. Types with Drop impose additional restrictions, discussed in the resource-management lessons.
The structs chapter covers field access and literals. The structs reference supplies the declaration rules.
Methods declare an access contract¶
An impl Reading block attaches operations to the type. Self names the implementing type, while lowercase self is the receiver value. An associated function without a receiver uses Reading::function(...); a method is normally called with value.method(...).
| Receiver | Purpose in this example | Caller requirement |
|---|---|---|
&self |
Inspect temperature | A valid shared borrow |
&mut self |
Replace temperature | Exclusive mutable access |
self |
Consume Reading and return its label | Ownership of the Reading |
Method-call syntax can borrow the receiver automatically: reading.celsius() borrows rather than moving Reading. The setter requires a mutable binding in our main program. into_label consumes it, so no subsequent whole-Reading use is allowed. We did not derive Copy or Clone; merely defining methods does not change those rules.
See method syntax and associated items.
Other struct forms¶
Tuple structs such as struct Millidegrees(i32); give a type identity to positional fields. Unlike a type alias for i32, this is a distinct type, which can help avoid mixing units. Unit-like structs such as struct Marker; have no fields and can represent a marker or a behaviour-bearing type. We return to these choices after traits are introduced.
Struct update syntax, Reading { millidegrees: 50_000, ..old }, fills unspecified fields from old. It is not an implicit clone: a non-Copy label field moves. Prefer explicit construction while learning ownership rather than assuming an update leaves the original struct intact.
Deliberately failing consumed receiver¶
Use a separate scratch project for this complete example:
cargo check should reject use of the moved receiver (E0382). A borrowed getter can be appropriate when the caller wants inspection rather than ownership; that is a different contract, not a reason to clone every value automatically.
Exercises and checks¶
- Remove
mutfrom the main program's reading binding: the setter should fail to compile (E0596). Restore it. - Change the setter argument to
0: expectupdated: 0.0 C; the label is unchanged. Check that the setter has no hidden range-validation policy. - Construct Reading with a literal rather than
new: the fourth test demonstrates that both forms are valid here. - Before consuming the value, predict whether the field access, print and celsius call move or borrow it. Formatting borrows the label;
into_labeltransfers it.
If a field is missing from a literal, supply it explicitly rather than assuming an automatic default. If mutation is rejected, inspect the receiver contract and overlapping references. Structs have neither inheritance nor automatically generated getters; abstraction through traits comes later.
Verification and next step¶
On October 10, 2026, the example was verified on a Raspberry Pi 4B with 64-bit user space, kernel 6.18.50+rpt-rpi-v8, Rust and Cargo 1.99.0, and edition 2024. Cargo check, all 4 tests, formatting and debug/release output comparisons passed. The consumed-receiver and missing-mut examples were rejected with E0382 and E0596 respectively. The zero-update exercise also ran with the expected output. No sensor or performance measurement is involved.
Continue with enums, match and patterns to represent alternative states.