- traits.md: iterator.rs anchor L76→L42 (pub const trait Iterator) - traits.md: deref.rs anchor L60→L139 (pub const trait Deref) - error-handling.md: fix variant names CliError::Io→IoError, Parse→ParseError to match actual stdlib doc example at convert/mod.rs:557 - concurrency.md: mutex.rs and rwlock.rs moved to sync/poison/ subtree (3 link updates: mutex×2, rwlock×1)
19 KiB
Rust Trait Patterns
Patterns for trait design in Rust, extracted from the standard library source at rust-lang/rust.
Source: rust-lang/rust at commit
f53b654
Stats: 241 public traits, 4,387 trait implementations, 508 From impls, 895 Iterator impls, 269 Display impls, 226 Default impls.
1. Small Traits (One Required Method)
Source:
library/core/src/iter/traits/iterator.rs#L42
// library/core/src/iter/traits/iterator.rs
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// 70+ provided methods built on top of `next()`...
fn map<B, F>(self, f: F) -> Map<Self, F> { ... }
fn filter<P>(self, predicate: P) -> Filter<Self, P> { ... }
fn collect<B>(self) -> B where B: FromIterator<Self::Item> { ... }
// ...
}
Iterator has 1 required method (next) and 70+ provided methods.
Implementing Iterator for your type requires writing ONE function.
Why
Minimize the cost of implementing the trait while maximizing the value. One required method means trivial to implement; provided methods give the full API for free. 895 Iterator implementations exist in the stdlib because it's so easy.
When to Use
Triggers:
- You want many types to implement your trait
- There's a clear "minimal kernel" that all implementations share
- Other methods can be defined in terms of that kernel
Example — before:
// Fat trait — every implementor must write 5 methods
trait Sequence {
fn next(&mut self) -> Option<Self::Item>;
fn size_hint(&self) -> (usize, Option<usize>);
fn count(self) -> usize;
fn last(self) -> Option<Self::Item>;
fn nth(&mut self, n: usize) -> Option<Self::Item>;
}
// Result: few types implement it because it's too much work
Example — after:
// Minimal required, rest provided
trait Sequence {
type Item;
fn next(&mut self) -> Option<Self::Item>; // ONE required method
// Provided defaults — free for all implementors:
fn size_hint(&self) -> (usize, Option<usize>) { (0, None) }
fn count(self) -> usize { self.fold(0, |c, _| c + 1) }
fn last(self) -> Option<Self::Item> { self.fold(None, |_, x| Some(x)) }
fn nth(&mut self, n: usize) -> Option<Self::Item> {
for _ in 0..n { self.next(); }
self.next()
}
}
When NOT to Use
Don't use this when:
- Methods are genuinely independent (can't be derived from each other)
- Implementors SHOULD be forced to think about each method
(e.g.,
PartialOrdrequirespartial_cmpbecause you can't derive comparison from equality) - The "default" would be wrong for most types
2. Derive Macros for Standard Traits
Source:
1,116 #[derive(...)] annotations in library/. Top derives:
Clone (880), Debug (869), Copy (537), PartialEq (388), Eq (285).
// Typical stdlib type:
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Duration {
secs: u64,
nanos: Nanoseconds,
}
Why
Derive generates correct implementations mechanically. Hand-writing Clone/Debug/PartialEq is error-prone and repetitive. The derive order is a convention: Debug first, then Clone/Copy, then comparison traits, then Hash.
When to Use
Triggers:
- Your struct/enum fields all implement the trait you want to derive
- The derived behavior is correct (field-by-field comparison, etc.)
- You want standard trait behavior without custom logic
Example — before:
struct Point { x: f64, y: f64 }
impl fmt::Debug for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Point")
.field("x", &self.x)
.field("y", &self.y)
.finish()
}
}
impl Clone for Point { ... }
impl PartialEq for Point { ... }
// 30 lines of boilerplate for standard behavior
Example — after:
#[derive(Debug, Clone, PartialEq)]
struct Point { x: f64, y: f64 }
// One line. Same result.
When NOT to Use
Don't use this when:
- You need custom comparison logic (e.g., ignore certain fields)
- Your type contains types that don't implement the trait
- Derived behavior would be wrong (e.g., floating point and Eq)
Anti-pattern
// DON'T: Derive Eq on floats
#[derive(PartialEq, Eq)] // COMPILE ERROR: f64 doesn't impl Eq
struct Measurement { value: f64 }
// DON'T: Derive PartialEq when it would be semantically wrong
#[derive(PartialEq)]
struct Handle { id: u64, cached_name: String }
// Do you really want cached_name to affect equality?
3. From/Into for Type Conversions
Source:
library/core/src/convert/mod.rs#L460
508 From<> implementations in the stdlib.
// Blanket impl: implementing From gives you Into for free
impl<T, U> Into<U> for T where U: From<T> {
fn into(self) -> U { U::from(self) }
}
Why
From/Into is the standard conversion mechanism. Always implement
From (not Into) because you get Into for free via the blanket
impl. The stdlib says: "This trait must not fail. If the conversion
can fail, use TryFrom."
When to Use
Triggers:
- Conversion is infallible and lossless
- Converting between wrapper types (newtype → inner, inner → newtype)
- Error type conversion (see error handling patterns)
Example — before:
// Ad-hoc conversion methods with inconsistent names
impl Celsius {
fn to_fahrenheit(&self) -> Fahrenheit { ... }
}
impl Fahrenheit {
fn from_celsius(c: &Celsius) -> Fahrenheit { ... }
}
// Users must remember: is it .to_X() or ::from_X()?
Example — after:
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
// Now works with: Fahrenheit::from(c), c.into(), and generic bounds
When NOT to Use
Don't use this when:
- Conversion can fail (use
TryFrom) - Conversion is lossy (u32 → u16 can truncate)
- It's an expensive operation that shouldn't happen implicitly
4. Default for Sensible Zero Values
Source:
226 Default implementations in the stdlib.
pub trait Default: Sized {
fn default() -> Self;
}
Why
Provides a canonical "empty" or "zero" value. Used by ..Default::default()
struct update syntax, Option::unwrap_or_default(), collection
initialization, and generic code that needs to create values.
When to Use
Triggers:
- Your type has an obvious "empty" state (empty string, zero, nil)
- You want
..Default::default()to work for partial struct init - Your type is used in generics bounded by
Default
Example — before:
let config = Config {
timeout: Duration::from_secs(30),
retries: 3,
verbose: false,
port: 8080,
host: String::new(),
};
// Must specify EVERY field even for "default" values
Example — after:
#[derive(Default)]
struct Config {
timeout: Duration, // Default: 0s (may need custom impl)
retries: u32, // Default: 0
verbose: bool, // Default: false
port: u16, // Default: 0
host: String, // Default: ""
}
let config = Config {
port: 8080,
retries: 3,
..Default::default() // everything else gets defaults
};
When NOT to Use
Don't use this when:
- There's no meaningful default (what's a "default" database URL?)
- The zero value would be dangerous (default timeout of 0 = infinite)
- You WANT users to think about every field
5. Display for Human-Readable Output
Source:
library/core/src/fmt/mod.rs#L1187
269 Display implementations in the stdlib.
pub trait Display: PointeeSized {
fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}
Why
Display is for user-facing output (what println!("{}", x) uses).
Debug is for developer-facing output (println!("{:?}", x)). Every
public type should implement both. Error trait REQUIRES Display.
When to Use
Triggers:
- Your type will be printed/logged for end users
- Your type implements Error (Display is required)
- You want
format!("{}", value)to work - The output should be human-readable (not debug-formatted)
Example — before:
// Only Debug — ugly output for users
#[derive(Debug)]
struct Version { major: u32, minor: u32, patch: u32 }
println!("{:?}", v); // "Version { major: 1, minor: 2, patch: 3 }" — ugly
Example — after:
#[derive(Debug)]
struct Version { major: u32, minor: u32, patch: u32 }
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
println!("{}", v); // "1.2.3" — clean
When NOT to Use
Don't use this when:
- There's no single canonical human representation
- The type is internal and never shown to users
- Debug output is sufficient (use
#[derive(Debug)])
6. Iterator Implementation
Source:
library/core/src/iter/traits/iterator.rs
895 Iterator implementations in the stdlib.
Why
Implementing Iterator gives you the entire iterator ecosystem for
free: .map(), .filter(), .collect(), for loops, and
composability with every other iterator adapter.
When to Use
Triggers:
- Your type represents a sequence of values
- Users should be able to loop over it with
for - You want composability with the iterator ecosystem
Example — before:
struct Fibonacci { a: u64, b: u64 }
impl Fibonacci {
fn next_value(&mut self) -> u64 {
let result = self.a;
let new_b = self.a + self.b;
self.a = self.b;
self.b = new_b;
result
}
}
// Can't use with for loops, map, filter, take, collect...
Example — after:
struct Fibonacci { a: u64, b: u64 }
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
let result = self.a;
let new_b = self.a.checked_add(self.b)?; // overflow → end
self.a = self.b;
self.b = new_b;
Some(result)
}
}
// Now works with everything:
let first_10: Vec<u64> = Fibonacci { a: 0, b: 1 }.take(10).collect();
When NOT to Use
Don't use this when:
- Your type doesn't represent a sequence
- Access is random (implement Index instead)
- The "iteration" has side effects that shouldn't compose
7. Deref for Smart Pointer Transparency
Source:
library/core/src/ops/deref.rs#L139
pub trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}
Why
Deref coercion makes smart pointers transparent: Box<T> behaves
like T, String behaves like str, Vec<T> behaves like [T].
The compiler inserts .deref() calls implicitly.
When to Use
Triggers:
- Your type is a smart pointer or wrapper around a single inner value
- You want method calls to "pass through" to the inner type
- There's a clear "target" type that your wrapper contains
Example — before:
struct MyString(Vec<u8>);
impl MyString {
fn as_str(&self) -> &str {
std::str::from_utf8(&self.0).unwrap()
}
fn len(&self) -> usize { self.as_str().len() }
fn is_empty(&self) -> bool { self.as_str().is_empty() }
fn contains(&self, pat: &str) -> bool { self.as_str().contains(pat) }
// Must manually delegate EVERY str method...
}
Example — after:
struct MyString(Vec<u8>);
impl Deref for MyString {
type Target = str;
fn deref(&self) -> &str {
std::str::from_utf8(&self.0).unwrap()
}
}
// Now ALL str methods work automatically:
// my_string.len(), my_string.contains("x"), my_string.to_uppercase()
When NOT to Use
Don't use this when:
- Your type is not semantically a wrapper/pointer
- The deref target is surprising to users
- You're using Deref as inheritance (Rust doesn't have inheritance — this abuse leads to confusing APIs)
Anti-pattern
// DON'T: Use Deref as "inheritance"
struct Dog { name: String }
struct GuideDog { dog: Dog, handler: String }
impl Deref for GuideDog {
type Target = Dog;
fn deref(&self) -> &Dog { &self.dog }
}
// This makes GuideDog "look like" Dog — misleading.
// Use composition + explicit delegation instead.
8. Sealed Traits (Restrict Implementors)
Source:
library/portable-simd/crates/core_simd/src/cast.rs
mod sealed {
pub trait Sealed {}
}
pub trait SimdCast: Sealed + SimdElement {}
Why
A sealed trait can only be implemented within its defining crate. External crates can USE the trait but not IMPLEMENT it. This gives the library author freedom to add methods without breaking changes.
When to Use
Triggers:
- You want to add trait methods in future versions without breaking downstream code
- The set of valid implementors is fixed and known
- You're publishing a library and need API stability guarantees
Example — before:
// Public trait — anyone can implement it
pub trait Codec {
fn encode(&self, data: &[u8]) -> Vec<u8>;
// Can NEVER add decode() without breaking all implementors
}
Example — after:
mod private { pub trait Sealed {} }
pub trait Codec: private::Sealed {
fn encode(&self, data: &[u8]) -> Vec<u8>;
fn decode(&self, data: &[u8]) -> Vec<u8>; // safe to add later!
}
// Only YOUR crate can implement Codec because only YOUR crate
// can implement private::Sealed
impl private::Sealed for JsonCodec {}
impl Codec for JsonCodec { ... }
When NOT to Use
Don't use this when:
- Users should be able to add their own implementations
- You're writing application code (not a library)
- The trait is intentionally an extension point
9. #[must_use] on Types and Functions
Source:
1,963 #[must_use] annotations across the stdlib.
#[must_use = "this `Result` may be an `Err` variant, which should be handled"]
pub enum Result<T, E> { ... }
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct Map<I, F> { ... }
Why
The compiler warns when a #[must_use] value is ignored. This
catches bugs where you call a function for its return value but
forget to use it. Iterator adapters are all #[must_use] because
they're lazy — creating a .map() does nothing until collected.
When to Use
Triggers:
- Ignoring the return value is always a bug
- The function/type is lazy (nothing happens without consumption)
- Builder methods that return a new value (not &mut self)
Example — before:
fn sorted(mut v: Vec<i32>) -> Vec<i32> {
v.sort();
v
}
let data = vec![3, 1, 2];
sorted(data); // BUG: result discarded, data is gone, nothing happened
Example — after:
#[must_use = "sorted returns the sorted vec; the original is consumed"]
fn sorted(mut v: Vec<i32>) -> Vec<i32> {
v.sort();
v
}
sorted(data); // COMPILER WARNING: unused return value of `sorted`
When NOT to Use
Don't use this when:
- The function is called for side effects (e.g.,
println!) - The return value is informational but not critical
10. Blanket Implementations
Source:
library/core/src/convert/mod.rs (From → Into blanket)
// Anyone who implements From gets Into for free:
impl<T, U> Into<U> for T where U: From<T> {
fn into(self) -> U { U::from(self) }
}
// Every type can be converted from itself:
impl<T> From<T> for T {
fn from(t: T) -> T { t }
}
Why
Blanket impls provide functionality to ALL types that satisfy a bound, without requiring each type to opt in. This is how Rust scales — you implement one trait and get many others for free.
When to Use
Triggers:
- Trait B can be derived from trait A for ALL types
- You want to reduce the number of impls users must write
- There's a clear mathematical relationship (From ↔ Into)
When NOT to Use
Don't use this when:
- The blanket would conflict with specific implementations
- Not all types satisfying the bound actually want the blanket
- The blanket would surprise users or violate expectations
Summary: Trait Design Decision Tree
Designing a new trait?
├── How many required methods?
│ ├── Can provide defaults built on a kernel → 1 required + many provided
│ └── Methods are independent → require each one
├── Who should implement it?
│ ├── Everyone → keep it simple, consider derive macro
│ ├── Only your crate → seal it (mod private { Sealed })
│ └── External + internal → leave it open, use #[non_exhaustive] on enums
├── Standard traits to implement on your types?
│ ├── Always: Debug
│ ├── If meaningful: Clone, PartialEq, Eq, Hash, Default
│ ├── If printable: Display
│ ├── If sequential: Iterator
│ ├── If wrapper: Deref
│ └── If convertible: From (not Into)
└── Should result be ignored?
└── Never → add #[must_use]
| When you need to... | Use |
|---|---|
| Provide rich API from minimal impl | One required method + provided defaults |
| Standard trait behavior | #[derive(Debug, Clone, PartialEq, ...)] |
| Type conversion | impl From<Source> for Target |
| Sensible zero/empty value | impl Default (or derive) |
| Human-readable output | impl Display |
| Iteration/composition | impl Iterator |
| Smart pointer transparency | impl Deref |
| Restrict implementors to your crate | Sealed trait pattern |
| Prevent value discard | #[must_use] |
| Free impls for all qualifying types | Blanket implementation |
See also:
- error-handling.md — Error trait + From impls
- ownership.md — Clone, Copy, Drop trait usage
- api-design.md — Public trait design guidelines