Rust Enums, Match and Patterns¶
A Rust enum describes alternatives: a reading can contain a measurement, be missing, or carry an invalid-input explanation. This lesson uses match and patterns to handle those states explicitly instead of confusing a missing reading with a real zero.
Prerequisites and outcome¶
Complete structs and methods. You will attach data to enum variants, match exhaustively, borrow fields through patterns and use if let and let else for focused cases. The input is fixture data; the threshold of 60,000 millidegrees is an exercise policy, not a Pi cooling or throttling limit.
Represent alternatives with a Rust enum¶
Our ReadingState has three variants. Measured uses named fields, Missing has no data, and Invalid contains owned text. Each value is one variant, not all three at once. Unlike several independent flags, this representation cannot express “missing and measured” simultaneously.
That guarantee has limits. Measured still accepts any i32; an enum does not establish sensor accuracy or validate an operating range. Nor does it decide when a measurement becomes missing: application logic supplies that state.
Replace src/main.rs with:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | |
Expected output:
Match is exhaustive and produces a value¶
Each match arm consists of a pattern and a result. Rust checks that every possible value is handled. Here, the match expression returns a string label; all arms must produce compatible values.
The first arm binds millidegrees and uses a guard after if. The fallback Measured arm remains necessary: a guard does not prove every possible measured value is covered. Arms are considered in order, so putting the unguarded Measured arm first would prevent the guarded one from doing useful work.
.. ignores remaining named fields; _ ignores a value without binding it. A catch-all arm can be useful, but matching each variant explicitly makes an added variant prompt a review of this decision. A wildcard can hide that reminder.
The match chapter explains these rules. Enums can also have associated functions and methods in impl blocks, just like the struct in lesson 6.
The returned &'static str is a reference to one of the fixed string literals, not newly allocated text borrowed from a local String. The lifetime lesson later explains the annotation; it does not prolong the lifetime of arbitrary local data.
Destructuring, alternatives and ranges¶
Patterns also extract tuples and structs, as previous lessons demonstrated. 10 | 11 accepts either literal, 0..=9 matches an inclusive range, and value @ 0..=9 both checks the range and binds the matching integer. The final test exercises their boundaries. These are patterns, not Boolean expressions pasted into a match arm.
_ discards without introducing a name. A name beginning with an underscore is still a binding and can move a non-Copy value; it merely signals intentional non-use. Explicit bindings and borrowed matches should reflect the ownership you need. Or-patterns must introduce compatible bindings in their alternatives.
Patterns can borrow or move¶
We match a &ReadingState, so the field bindings use borrowed access: millidegrees is an &i32, and reason is an &String. Dereferencing the integer in the guard makes the comparison explicit. This behaviour is part of pattern binding modes, often called match ergonomics; it does not clone fields.
Matching an owned enum by value can instead move non-Copy fields. Use a borrowed match when inspecting a state that the caller must keep. Adding mut to a binding is not a replacement for choosing an appropriate reference.
In edition 2024, explicit binding modifiers have additional restrictions when the default mode is already borrowing. Start with the simple borrowed patterns shown here rather than adding ref mechanically. See the patterns reference for binding modes and their edition rules.
If let and let else have different jobs¶
if let handles one interesting pattern and skips the rest unless you provide an else branch. We use it to print only invalid-input reasons; it does not replace the exhaustive classification.
let ... else either binds the requested pattern for the following code or exits that path. Its else block must diverge, for example with return, break where appropriate, or panic. The main program uses return, so print_measured reports absence without manufacturing a numeric reading.
A plain let (count, value) = tuple; accepts a pattern that always matches that tuple shape. An enum-specific pattern can fail to match a different variant and is refutable; it needs a context such as match, if let or let else. The official concise control-flow chapter describes the focused forms.
Deliberately failing incomplete match¶
In a separate scratch project, replace src/main.rs with this complete program:
cargo check should reject the non-exhaustive match (E0004), even though this main function only supplies Ready. The function accepts any State, and its contract must account for Missing too.
Exercises and checks¶
- Remove the unguarded Measured arm from classify: expect E0004. The guard alone is not exhaustive.
- Make the else block in print_measured print a message but not return: expect E0308 because the else branch does not diverge. Restore the return.
- Change the fixture's first measurement to
0: expect a real measured zero, notmissing. Tests preserve the distinction. - Add a new
Stalevariant: explicitly add an arm before restoring the original exercise. Decide whether a stale value needs its own data rather than a second independent Boolean flag.
If an integer binding is unexpectedly a reference, inspect the matched expression's type before adding a clone. If moving a String makes the enum unusable, decide whether ownership transfer was really intended.
Verification and next step¶
On October 10, 2026, this lesson 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 5 tests, formatting and debug/release output comparisons passed. The zero-measurement exercise also ran. The incomplete match, missing unguarded arm, added variant and non-diverging else exercises failed for their expected reasons. The program does not acquire or parse sensor data.
Continue with strings, vectors and hash maps to work with variable-size input.