- name
- loom-rust
- description
- Rust language expertise for idiomatic, production-quality code.
- triggers
- ["rust","cargo","rustc","ownership","borrowing","lifetime","trait","impl","struct","enum","Result","Option","async","await","tokio","serde","clap","thiserror","anyhow","Arc","Mutex","RwLock","RefCell","Box","Rc","Vec","HashMap","HashSet","String","derive","macro"]
# Rust Language Expertise
## Overview
Idiomatic, production-quality Rust for an engineer who already knows ownership/borrowing. This skill is decision rules, gotchas, and the traps that cost hours: async/`Send` correctness, error-handling architecture, serde pitfalls, and edition-2024 features. Assumes the borrow checker itself is not the problem — using it *well* is.
## Error Handling: anyhow vs thiserror
**The decision rule:** will a caller ever `match` on the error to recover? → `thiserror` (concrete, matchable enum). Does the error only bubble up to be logged/reported? → `anyhow` (type-erased, context chains). **Never put `anyhow::Error` in a library's public API** — it erases the type and denies callers any recovery. Libraries expose `thiserror` enums; binaries/apps consume with `anyhow`.
```rust
// Library boundary: thiserror. Each variant = a distinct, matchable failure mode.
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ParseError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error), // #[from] gives `?` conversion for free
#[error("invalid syntax at {line}:{column}: {message}")]
Syntax { line: usize, column: usize, message: String },
#[error("unexpected token: expected {expected}, found {found}")]
Unexpected { expected: String, found: String },
}
```
```rust
// Application code: anyhow. `?` unifies heterogeneous errors; .context adds a chain.
use anyhow::{Context, Result, bail, ensure};
fn load_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("reading config: {path}"))?; // with_context: lazy, use for allocating msgs
let config: Config = toml::from_str(&content).context("parsing config TOML")?; // context: eager literal
ensure!(!config.name.is_empty(), "config name cannot be empty"); // returns Err on false
if config.port == 0 { bail!("port must be non-zero"); } // early return with Err
Ok(config)
}
```
- `context` (eager, takes a value) vs `with_context(|| ...)` (lazy closure) — use `with_context` whenever the message allocates (`format!`), else you pay the cost on the success path too.
- `#[from]` generates `From` for `?`; `#[error(transparent)]` forwards `Display`/`source` to the inner error (use for a pass-through variant).
- Option→Result: `.ok_or_else(|| Error::NotFound(id.to_string()))?`. Prefer `ok_or_else` (lazy) over `ok_or` when the error allocates.
- Collecting: `iter.map(f).collect::<Result<Vec<_>>>()` stops at the first `Err`; collect into `Vec<Result<_>>` to keep all outcomes.
- **`thiserror` 2.0:** must be a **direct** dependency (not transitive); format strings dropped raw-identifier support (`{type}`, not `{r#type}`); field trait bounds no longer inferred when shadowed by a format arg. New: `no_std` via `default-features = false`, out-of-line `#[error(fmt = path)]`, per-variant `#[error(transparent)]`. Pin `thiserror = "2"`.
## Traits, Generics & Lifetimes
```rust
// Accept the widest input, return the narrowest: Into<T> in bounds, From impls only.
// std's blanket `impl<T, U: From<T>> Into<U> for T` means every From yields Into for free;
// implementing Into manually does NOT grant the reverse From. So implement From, bound on Into.
fn store(&mut self, id: impl Into<String>) { self.id = id.into(); }
// where-clauses for readability on complex bounds
fn merge<V>(a: impl IntoIterator<Item = V>, b: impl IntoIterator<Item = V>) -> Vec<V>
where V: Ord {
let mut v: Vec<V> = a.into_iter().chain(b).collect();
v.sort();
v.dedup();
v
}
// Blanket impls are legal ONLY when the trait or the type is local to your crate (orphan rule).
trait Summary { fn summary(&self) -> String; }
impl<T: std::fmt::Display> Summary for T { fn summary(&self) -> String { self.to_string() } }
```
- **Never implement `ToString`** — std ships `impl<T: Display + ?Sized> ToString for T`. A manual impl conflicts with std's and violates the orphan rule (E0119, won't compile). Implement `Display`; `to_string()` comes free.
- **`impl Trait`/generics (static) vs `dyn Trait` (dynamic):** generics monomorphize — inlinable, but N copies bloat the binary if instantiated widely. `dyn Trait` is a vtable indirect call (no inlining) but one code path and heterogeneous collections (`Vec<Box<dyn Stage>>`). RPIT (`-> impl Trait`) avoids boxing but fixes one concrete type; `Box<dyn Trait>` allows runtime variation at an allocation.
- **Implement `Deref`/`DerefMut` only for smart pointers.** Deref coercion is implicit and transitive; on a plain wrapper it silently exposes the target's entire API (name collisions, unpredictable surface). Write explicit delegation methods for wrappers.
- **Name conversions by cost (API Guidelines C-CONV/C-GETTER):** `as_` = free borrow reinterpret (`str::as_bytes`); `to_` = expensive/allocating (`str::to_uppercase`); `into_` = consuming owned→owned (`String::into_bytes`). Getters take the field name with **no** `get_` prefix (`first()`, not `get_first()`).
- **Lifetime elision:** `fn get(&self, k: &str) -> Option<&str>` desugars to output borrowing `&self`. Only annotate when multiple input lifetimes make the output ambiguous.
- **Borrow-checker fights:** to mutate two struct fields at once, **destructure** rather than call `&mut self` methods: `let Self { a, b } = self;` gives independent borrows. For slices, `split_at_mut`. See also the `entry` API below.
## Iterators
Prefer iterator chains over index loops: no bounds checks, no off-by-one, and the optimizer fuses them into tight loops.
```rust
let emails: Vec<String> = users.into_iter()
.filter(|u| u.active)
.filter_map(|u| u.email) // yields T from Option<T>, dropping None
.collect();
let (evens, odds): (Vec<_>, Vec<_>) = nums.iter().partition(|&&x| x % 2 == 0);
let sum: i32 = nums.iter().sum();
let first_even = nums.iter().find(|&&x| x % 2 == 0);
```
- Use `.flatten()` on an iterator of `Option`/`Result`, not `.filter_map(|x| x)` — the identity closure is a `clippy::filter_map_identity` error under `-D warnings`. (`filter_map` with a *real* closure is idiomatic.)
- `.collect::<Result<Vec<_>>>()` short-circuits on the first `Err`; `.collect::<Vec<Result<_>>>()` keeps every outcome.
- Reach for `itertools` (`.chunks`, `.group_by`, `.dedup_by`, `.sorted`, `.unique`) before hand-rolling; `.try_fold` for early-exit accumulation.
- Custom iterators: implement `Iterator::next` returning `Option<Self::Item>` — you get `map`/`filter`/`collect` etc. for free.
## Serde
```rust
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Config {
name: String,
#[serde(default)] enabled: bool, // missing → Default
#[serde(default = "default_timeout")] timeout: u64, // missing → fn value
#[serde(skip_serializing_if = "Option::is_none")] description: Option<String>,
#[serde(skip)] runtime_state: Option<String>, // never (de)serialized
#[serde(rename = "userId")] user_id: String,
#[serde(flatten)] extra: std::collections::HashMap<String, String>, // absorbs unknown keys
}
fn default_timeout() -> u64 { 30 }
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")] // enum variants / fields in one shot
enum Status { Active, Inactive, Pending }
```
**Enum tagging — pick deliberately, it defines the wire format:**
| Representation | Attribute | Wire shape |
| --- | --- | --- |
| Externally tagged (default) | *(none)* | `{"Text": {"content": "..."}}` |
| Internally tagged | `#[serde(tag = "type")]` | `{"type": "Text", "content": "..."}` |
| Adjacently tagged | `#[serde(tag = "t", content = "c")]` | `{"t": "Text", "c": {...}}` |
| Untagged | `#[serde(untagged)]` | bare value, variant inferred |
⚠ **`#[serde(untagged)]` silently matches the FIRST variant that deserializes** — declaration order decides which variant wins. An earlier variant with an overlapping shape wins with no ambiguity error; failures produce the useless `data did not match any variant`; it is slow in index formats (bincode). Order variants **most-specific first** and add per-variant round-trip tests. Internally tagged also can't represent newtype-of-non-struct variants. Prefer a real tag whenever you control the format.
- Custom (de)serialize: `#[serde(with = "module")]`, or `serialize_with`/`deserialize_with` per field; whole-type via `impl Serialize`/`Deserialize`. `chrono` ships helpers (`chrono::serde::ts_seconds`).
- **`serde_yaml` is DEPRECATED** (archived at 0.9.34, 2024-03-25; its `yaml-rust` dep is RUSTSEC-2024-0320). The `serde_yml` fork has an unsoundness advisory (RUSTSEC-2025-0068, ≤0.0.12). Prefer TOML/JSON for Rust-native config; if YAML is mandatory use the maintained `yaml-rust2`, and verify the API (`serde-saphyr` does not implement serde's traits — not a drop-in).
## Async & Concurrency (Tokio)
```rust
use std::sync::Arc;
use tokio::sync::{mpsc, RwLock};
// std::sync::Mutex for state NOT held across .await (cheaper); tokio's async Mutex/RwLock
// ONLY when the guard genuinely must span an await point.
struct AppState {
counter: Arc<std::sync::Mutex<u64>>, // never crosses .await
cache: Arc<RwLock<std::collections::HashMap<String, String>>>, // crosses .await
}
impl AppState {
fn increment(&self) -> u64 {
let mut c = self.counter.lock().unwrap(); // std guard, dropped before any await
*c += 1;
*c
}
async fn get_cached(&self, key: &str) -> Option<String> {
self.cache.read().await.get(key).cloned() // tokio RwLock held across await
}
}
```
⚠ **Never hold a `std::sync::MutexGuard` across an `.await`.** The guard is `!Send`, so a future holding one is `!Send` and `tokio::spawn` on the multi-thread runtime rejects it at compile time. Worse: on a single task / `current_thread` it *compiles* but the worker blocks on the lock and can never poll the task that would release it — a **runtime deadlock the compiler does not catch**. Drop the guard in an explicit `{ }` scope before the await, or use `tokio::sync::Mutex` (guard is `Send`, at the cost of async locking) only when it must span the await.
⚠ **The same trap with `RefCell`:** a `Ref`/`RefMut` held across `.await` can hit a `BorrowMutError` **panic** when another task on the runtime re-enters the same cell. `RefCell` is `!Send` so it can't cross into a multi-thread `spawn` anyway — drop the borrow before awaiting.
⚠ **`tokio::spawn` requires `Send + 'static`.** The future must OWN its data (`async move { }`) — borrowed locals, `Rc`, `RefCell` break it. Subtle: a non-`Send` value created *and dropped* within an await-free span can still poison `Send` inference (auto-trait analysis spans the whole async block) — force an early drop with an explicit `{ }` scope.
⚠ **`tokio::select!` drops (cancels) losing branches at their suspension point** — most async ops are NOT cancel-safe, and any state in the loser's future-local vars is silently lost each loop iteration.
- **NOT cancel-safe:** `read_exact`, `read_to_end`, `write_all`, `Mutex::lock`, `RwLock::read/write`, `Semaphore::acquire`.
- **Cancel-safe:** `mpsc::Receiver::recv`, `TcpListener::accept`, `AsyncReadExt::read` (returns partial).
- Put only cancel-safe ops directly in `select!` branches; store resumable state in a struct field, not a future-local.
```rust
tokio::select! {
msg = rx.recv() => { /* recv is cancel-safe */ }
_ = shutdown.cancelled() => return,
}
```
⚠ **`spawn_blocking` is for blocking I/O, not CPU work at scale.** It runs on a separate blocking pool (default max 512, `Builder::max_blocking_threads`). Past the limit calls silently queue; a running blocking task **cannot be aborted**. For CPU-bound work use `rayon` and bridge back via a `oneshot`.
Auf GitHub ansehen