Rust Traits, Implementations and Associated Types¶
A Rust trait describes behaviour that a type agrees to provide; it is not a class containing shared fields. This lesson defines a reading source, gives integer and text fixtures different result types, and composes a named report without inheritance or runtime dispatch.
Prerequisites and outcome¶
Complete generics, bounds and const generics. You will implement required methods, reuse a default method, constrain an associated type, and recognise why two implementations cannot conflict. The example uses fixture data and stable Rust; hardware I/O and trait objects are separate topics.
What the Rust trait contract includes¶
Our Source trait declares an associated type Value, an associated constant UNIT, a required read method and a default summary method. Self refers to the implementing type, so Self::Value names that implementation's chosen value type. The read receiver is shared: callers borrow the source rather than consume it.
The trait reference explains required and default associated items. A default method can call required methods, but does not allocate fields or provide a parent object's storage. Implementations must supply the required items; the trait declaration's signatures are not optional conventions.
Build the complete fixture-source program¶
Keep edition = "2024" in Cargo.toml and 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 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | |
Expected output:
There are seven tests. Status constructs an owned String on each read; traits do not make that allocation disappear. Temperature copies an Option
Associated types versus generic trait parameters¶
Temperature chooses Value = i32; Status chooses Value = String. The function bound Source<Value = i32> accepts only a source whose associated type is exactly i32. Merely having a read method with a similar name does not implement Source.
For this non-generic Source trait, a particular implementing type has one chosen Value, not a caller-selected result type for every call. A trait declared with a parameter, such as Convert
<Temperature as Source>::UNIT is a fully qualified path. It states both the implementing type and the trait, useful when names would otherwise be ambiguous. UNIT belongs to the implementation, not to an individual instance. The 'static string reference here points to a string literal; it does not require each source object to live forever.
Defaults, supertraits and delegation¶
Neither Temperature nor Status overrides summary(), so each gets its default body. An implementation can replace that method while keeping its signature. Callers should rely on the documented trait contract rather than assume every implementation uses identical formatting or cost.
Report requires both Source and Named as supertraits. It can use their methods, but does not inherit their fields. Device satisfies them through explicit implementations, and its source field uses composition. Named does not need a bound on S; Source does, because it calls S's read method. See the supertrait rules.
Keep use sources::Source in scope for ordinary trait-method calls on Temperature or Status. The trait is declared in a different module, unlike their inherent methods. Fully qualified calls such as Source::summary(&missing) can select the trait explicitly. Adding pub to methods inside a trait impl is not how to export them; make the trait accessible and use its API.
Blanket implementations and coherence¶
The impl<T: Source + Named> Report for T {} line is a blanket implementation. Its empty body is valid because report_line has a default. Device
Adding an explicit Report implementation for Device
There is also an orphan check: implementing a foreign trait directly for a foreign type is rejected. Defining our own trait allows implementations on foreign types, and defining a local wrapper can allow foreign-trait implementations. Generic cases also have uncovered-parameter restrictions; “one local type anywhere” is not a complete rule. The implementation reference gives the precise check. A type alias for a foreign type does not create a local wrapper.
Derive is generated implementation code¶
Temperature derives Debug, Clone, PartialEq and Eq. Those generated implementations use its fields, so the fields must satisfy the relevant requirements. Eq is suitable for Option
The derive reference describes generated implementations and built-in traits. Display is not a built-in derive: user-facing text needs a chosen format, while Debug supports diagnostics. See the Display documentation. Derive macros are revisited in the macro lesson; there is no need to write a procedural macro here.
Deliberately failing: foreign trait on a foreign type¶
In a separate scratch project, replace src/main.rs with this complete program:
cargo check reports E0117: Display and Vec are both foreign to this crate. Matching a method signature does not remove the orphan rule. A local newtype such as struct Readings(Vec<i32>); gives you a distinct type whose Display implementation can deliberately format its contained vector.
Exercises and troubleshooting¶
- Remove
type Value = i32from Temperature's implementation: expect E0046 for the missing required item, rather than an inferred associated type. - Call read_integer on the Status-based device: expect E0271 because its Value is String, not i32. The general report still accepts it.
- Add
impl Report for Device<Temperature> {}: expect E0119 because the blanket implementation already applies. - Fully qualify Source in other bounds and type paths, then remove it from the root use list: expect E0599 at missing.summary(), even though the implementation remains present. Restore the import or use
sources::Source::summary(&missing); import Source directly from sources in the child tests if you remove the root name. - Repair the foreign-trait example with Readings, implement Display for that wrapper, and format
self.0. Print Readings containing[46700, 60000]; expect[46700, 60000]. Changing Vec's name through a type alias is not a repair. - Change only Temperature's field from Option
to Option : expect compile failure, including the derived Eq requirement. A wholesale float conversion needs an explicit equality policy and updated associated types, fixtures and tests.
If a required item is missing, check the trait declaration. If a method cannot be found, distinguish a missing implementation, an unsatisfied bound and a missing import. If impls conflict, inspect blanket coverage; hiding them in different modules does not avoid coherence.
Verification and next step¶
On October 10, 2026, the 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 seven tests, formatting and debug/release output comparisons passed. Fully qualified calls, the import repair and the local Display wrapper also passed. The orphan, missing associated type, mismatched value type, overlapping implementation, missing import and float Eq examples failed as expected, including E0117, E0046, E0271, E0119, E0599 and E0277 respectively. No live sensor or performance measurement is involved.
Next: lifetimes and borrowed APIs, explaining the validity of returned references. Source here also has an associated constant, which affects whether it can become a trait object; the dispatch lesson will explain that separately.