68. How not to write Rust: an anti-pattern catalog
Each entry shows a bad form, the problem, and a fix. These are the comments code reviewers leave most often on Rust from people coming from Go, C, or Java.
1. .clone() to silence the borrow checker
// fragment
// BAD: copies the whole Vec on every call, just to satisfy the compiler
fn total(items: Vec<Item>) -> u64 { items.iter().map(|i| i.price).sum() }
let t = total(cart.items.clone());
// GOOD: borrow
fn total(items: &[Item]) -> u64 { items.iter().map(|i| i.price).sum() }
let t = total(&cart.items);
Cloning is sometimes the right call (small values, or handing owned data to a thread). Do it on purpose, never just to make an error go away.
2. unwrap() on input you do not control
// fragment
// BAD: a malformed header crashes the request task (DoS, §51.2)
let len: usize = req.headers()["content-length"].to_str().unwrap().parse().unwrap();
// GOOD: errors are values
let len: usize = req.headers().get("content-length")
.and_then(|v| v.to_str().ok()).and_then(|s| s.parse().ok())
.ok_or(ApiError::BadRequest("content-length"))?;
3. Rc<RefCell<T>> (or Arc<Mutex<T>>) everywhere
Wrapping every object in shared mutable pointers rebuilds a garbage-collected
object graph, with runtime borrow panics and lock contention. Usually the
data has one natural owner. Pass &mut down the call stack, use indices
into a Vec (Ch 27), or send messages to a task that owns the state (an
actor, Ch 38).
4. Blocking inside async
// fragment
// BAD: stalls a Tokio worker thread, and every task scheduled on it
async fn handler() -> String { std::fs::read_to_string("big.json").unwrap() }
// GOOD
async fn handler() -> std::io::Result<String> { tokio::fs::read_to_string("big.json").await }
// For CPU work (hashing, compression, image processing): tokio::task::spawn_blocking (§31.1)
5. A std::sync::MutexGuard held across .await
// fragment
// BAD: the guard lives across the await (not Send; deadlock-prone)
let mut cache = state.cache.lock().unwrap();
let fresh = fetch(&key).await;
cache.insert(key, fresh);
// GOOD: lock, copy out, unlock, await, lock again
let cached = state.cache.lock().unwrap().get(&key).cloned();
let value = match cached { Some(v) => v, None => {
let fresh = fetch(&key).await;
state.cache.lock().unwrap().insert(key, fresh.clone());
fresh
}};
6. String parameters everywhere
fn greet(name: String) forces every caller to allocate. Take &str
(or impl AsRef<str>), and take String only when the function stores
the value. Then the caller decides whether to clone or move.
7. Stringly-typed errors
Result<T, String> cannot be matched on, has no source chain, and is
impossible to handle selectively. Use thiserror enums in libraries and
anyhow with .context() in binaries (§10.2–10.3).
8. Indexing loops instead of iterators
// fragment
// BAD: bounds check per access, and an off-by-one waiting to happen
for i in 0..v.len() { if v[i] > max { max = v[i]; } }
// GOOD
let max = v.iter().copied().max();
9. unsafe for performance before measuring
get_unchecked to avoid a bounds check that LLVM had already removed. A
hand-rolled transmute where from_le_bytes exists. Each unsafe block
needs a profile that justifies it and a SAFETY comment that proves it
(§33.2). Most of the time the safe version compiles to the same code.
10. Giant generic signatures with no dyn escape
Seven type parameters threaded through every function make compile errors
unreadable and compile times long. When the dispatch is not in a hot loop,
Box<dyn Trait> or an enum is simpler and just as fast in practice
(§17.2).
11. Ignoring #[must_use] results
let _ = tx.send(msg); and let _ = file.flush(); discard errors on
purpose. Make sure that is your intent. Writing the reason in a comment
(// receiver gone: client disconnected, nothing to do) turns a silent
bug into a documented decision.
12. Unbounded channels and collections fed by the network
mpsc::unbounded_channel(), a Vec that grows per message, or a
HashMap cache without eviction can be filled by an attacker or a slow
consumer until the OOM killer stops the process. Use bounded channels for
backpressure, cap sizes, and evict (Ch 27).