Provenance: Distilled and adapted from Apollo GraphQL's Rust Best Practices skill (MIT, © 2024 Apollo Graph, Inc.). See
../CREDITS.mdandATTRIBUTION.md. This is the idiom/readability plane — it sits under the concurrency/performance doctrine inSKILL.md, not against it. Error handling is intentionally not covered here (the SKILL.md "Error Handling & Resilience" section andmicrosoft-rust-guidelinesown that plane).
Load this when the question is about how Rust reads — borrowing, idiomatic
control flow, lint discipline, testing, dispatch choice, type-state, and docs.
For how fast it runs and how it scales across cores, stay in SKILL.md.
- Prefer
&Tover.clone(); pass&strnotString,&[T]notVec<T>/&Vec<T>. - Make ownership transfer explicit in the signature — never
let x = arg.clone()inside a function to fake ownership the caller should have handed you. - Clone deliberately, only when: you need a mutated copy and the original
(immutable snapshots),
Arc/Rcsharing, the callee API demands owned data, or avoiding a disruptive refactor in non-hot code. - Don't clone inside iterator closures (
.map(|x| x.clone())); call.cloned()/.copied()at the end of the chain instead.
- Derive
Copyonly when all fields areCopy, the type is "plain data" with no heap ownership, and it's small — ≤ 24 bytes / 2–3 machine words. - Good:
#[derive(Copy, Clone)] struct Point { x: f32, y: f32, z: f32 }. Bad: any struct holding aString/Vec. Enum size is its largest variant. - Arrays are stack-allocated and
Copyif their element is — but large[T; N]copies invite stack overflow; box or slice them.
let Some(x) = expr else { return / continue / break };when the missing case is expected and the divergent branch needs no info about the failure.if let … else { … }only when the else branch needs real computation.matchwhen you pattern-match innerT/E, use guards, or reshape the type.- Convert with
.ok()/.ok_or_else(), not a hand-writtenmatch. - Propagate with
?when you don't inspect theErr; transform/log with.inspect_err(…)+.map_err(…). - Prevent early allocation: prefer the
_else/_defaultfamily when the fallback allocates or computes —ok_or_else,unwrap_or_else,unwrap_or_default,map_or_else— overok_or,unwrap_or(Vec::new()),map_or(format!(…), …).
- Reach for iterator chains to transform collections /
Option/Result, compose steps,enumerate,windows/chunks, or fuse multiple sources without intermediate allocations. - Reach for a
forloop for early exit (break/continue/return), side-effecting iteration (logging, I/O), or when it simply reads clearer. - Iterators are lazy — nothing runs until a consumer (
.collect,.sum,.for_each). Prefer.iter()over.into_iter()unless you need ownership (and forCopyelement types). Prefer.sum()over.fold()for summation (the compiler specialises it). Don't.collect()just to throw the collection away.
The canonical CI policy is set in SKILL.md ("Tooling & Quality Gates":
clippy warnings-as-errors). This section is the per-lint detail under that policy —
not a competing policy.
- Daily / CI invocation:
cargo clippy --all-targets --all-features --locked -- -D warnings(add-W clippy::pedanticwhere you can stand the false-positive rate). - Encode levels in
Cargo.toml[lints.clippy]/[workspace.lints.clippy]with explicitpriorityso conflicting lints resolve deterministically. - Named lints worth respecting:
redundant_clone,clone_on_copy,needless_borrow,needless_collect,large_enum_variant(box the big variant),unnecessary_wraps,map_unwrap_or,manual_ok_or. - Fix, don't silence. Never
#[allow(clippy::…)]. Use#[expect(clippy::…)]with a justifying comment —expectre-warns once the lint no longer fires, so dead suppressions can't accumulate. Keep overrides local.
- One behaviour per test, ideally one assertion; a failing test should name exactly what broke.
- Name tests like sentences:
process_should_return_error_when_input_empty, or group undermod process { fn should_… }. Organise with#[cfg(test)] modsubmodules. - For matrices of inputs use
rstestcases with descriptive#[case::…]labels rather than many asserts in onefn. Onassert!/assert_eq!, pass a formatted message showing actual vs expected;Ok-path tests should print theErron failure.assert!(matches!(x, Pat))for shape checks;#[should_panic]only when panic is the contract. - Three test planes: unit (same module, sees privates, edge cases), integration
(
tests/, public API only, split binaries intomain.rs+lib.rs), doc-tests (///examples that run undercargo test— note: not undercargo nextest, usecargo test --doc). Doc-test attributes:no_run,should_panic,compile_fail. - Snapshot testing with
cargo instawhen output is structural/visual (generated code, serialised data, rendered HTML, CLI output). Prefer YAML snapshots; name them; keep them small and scoped (assert_yaml_snapshot!("app_config/http", cfg.http), not the whole object); redact unstable fields (timestamps, UUIDs); commit snapshots and review diffs. Don't snapshot primitives/flat structs — useassert_eq!.
- Static dispatch (
<T: Trait>/impl Trait) is the default: monomorphised, inlined, zero runtime cost — best for hot loops and call sites you control. - Dynamic dispatch (
Box<dyn Trait>,Arc<dyn Trait>,&dyn Trait) only when you genuinely need runtime polymorphism: heterogeneous collections, plugin/hot-swap architectures, or hiding internals behind a stable interface. - Ergonomics:
&dynwhen you don't need ownership;Arc<dyn>for cross-thread sharing; box at the API boundary, not internally; don't box prematurely inside structs. Trait objects must be object-safe (no generic methods, noSelf-returning, noSelf: Sized). If unsure, start generic and adddynonly when flexibility wins.
Encode states as types, not runtime flags — illegal operations become compile
errors, and PhantomData<State> is zero-cost (erased after compilation).
struct Disconnected;
struct Connected;
struct Client<State> {
stream: Option<std::net::TcpStream>,
_state: std::marker::PhantomData<State>,
}
impl Client<Disconnected> {
fn connect(addr: &str) -> std::io::Result<Client<Connected>> {
let stream = std::net::TcpStream::connect(addr)?;
Ok(Client { stream: Some(stream), _state: std::marker::PhantomData })
}
}
impl Client<Connected> {
fn send(&mut self, msg: &[u8]) { /* only a Connected client can send */ }
}Use it for compile-time state safety, builder "required fields before .build()",
and protocol state machines. Avoid it for trivial enum-like states, when it
explodes generic signatures, or when runtime flexibility is the point — "use it when it
saves bugs, not for cleverness."
//explains why — safety invariants (// SAFETY: …), performance quirks (// PERF: …), platform workarounds, links to an ADR/design doc. Don't restate the what (// increment i), don't leave walls of text, don't trust stale comments — read them in context and fix or delete.///(item) and//!(module/crate) explain what & how for public APIs, with# Examples,# Errors,# Panics,# Safetysections where relevant. Examples double as doc-tests.- Prefer structure and naming over commentary: split a function rather than
narrate its steps.
TODOs become tracked issues —// TODO(#42): …. - For libraries, enforce coverage with
#![deny(missing_docs)]and the rustdoc/clippy doc lints (missing_docs,missing_errors_doc,missing_panics_doc,missing_safety_doc,broken_intra_doc_links).
Group use declarations: std/core/alloc → external crates → workspace crates →
super::/crate::. Automate it in rustfmt.toml:
reorder_imports = true
imports_granularity = "Crate"
group_imports = "StdExternalCrate"(group_imports currently needs cargo +nightly fmt.)
Rust tracks thread safety via compiler-enforced auto-traits:
Sendindicates that ownership of a type can be transferred across thread boundaries.Syncindicates that references to a type (&T) can be shared safely across threads.
| Pointer Type | Description | Send / Sync | Use Case |
|---|---|---|---|
&T |
Shared reference | Send + Sync (if T: Sync) |
Immutable shared access. |
&mut T |
Exclusive mutable reference | Send + Sync (if T: Send + Sync) |
Exclusive temporary mutation. |
Box<T> |
Unique heap-allocated pointer | Send + Sync (if T: Send + Sync) |
Owned indirection / recursive types. |
Rc<T> |
Non-atomic reference-counted | Neither Send nor Sync |
Shared ownership within a single thread. |
Arc<T> |
Atomic reference-counted | Send + Sync (if T: Send + Sync) |
Shared ownership across multiple threads. |
Cell<T> |
Interior mutability for Copy types |
Send (if T: Send), not Sync |
Zero-overhead single-thread mutability. |
RefCell<T> |
Interior mutability (dynamic borrow) | Send (if T: Send), not Sync |
Single-thread runtime-checked mutation. |
Mutex<T> |
Thread-safe mutual exclusion lock | Send + Sync (if T: Send) |
Shared mutable access across threads. |
RwLock<T> |
Thread-safe readers-writer lock | Send + Sync (if T: Send + Sync) |
Read-heavy shared mutable thread access. |
Avoid complex custom setups with Option or unsafe blocks for lazy values. Use standard library cells:
- Single-Threaded (
std::cell):OnceCell<T>: Write-once container for single-threaded deferred initialization.LazyCell<T>(stabilized in Rust 1.80): Lazy value initialized via a closure on first deref.
- Thread-Safe / Shared (
std::sync):OnceLock<T>(stabilized in Rust 1.70): Thread-safeOnceCellfor global/shared resources.LazyLock<T>(stabilized in Rust 1.80): Thread-safeLazyCellfor lazy thread-safe globals.