Idioms, Patterns & API Guidelines
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Rust 1.99.0 and the 2024 edition, checked on 2026-10-06. Knowing the syntax is not the same as writing code that other Rust programmers find natural. This page collects the idioms and patterns that experienced Rust code uses — largely from the community Rust Design Patterns book, which is labelled as community material — and the official Rust API Guidelines, which describe how public library APIs should look. Examples use the shelf catalogue.
Idioms
Accept borrowed types
Take &str rather than &String, &[T] rather than &Vec<T>, &Path (or impl AsRef<Path>) rather than &PathBuf.
The borrowed form accepts more callers (literals, slices of arrays, sub-strings) at no cost, thanks to deref coercion.
fn title_case(title: &str) -> String { // not &String
title.split(' ').map(|w| {
let mut chars = w.chars();
chars.next().map(|c| c.to_uppercase().collect::<String>() + chars.as_str()).unwrap_or_default()
}).collect::<Vec<_>>().join(" ")
}
fn total_pages(pages: &[u32]) -> u32 { // not &Vec<u32>
pages.iter().sum()
}
fn main() {
let owned = String::from("the left hand of darkness");
println!("{} | {}", title_case(&owned), title_case("dune messiah"));
let pages: Vec<u32> = "412 180".split(' ').filter_map(|p| p.parse().ok()).collect();
println!("{} {}", total_pages(&pages), total_pages(&[96; 3])); // &Vec<u32> and &[u32; 3] both coerce
}
Constructors and Default
Rust has no constructors in the language; the convention is an associated function new (plus with_*/from_*
variants), and Default for types with a sensible empty value. Implement or derive Default whenever new() takes no
arguments, so the type works with ..Default::default(), mem::take and unwrap_or_default.
mem::take and mem::replace to move out of a &mut
When you own only a &mut to a value but need to move one of its fields (to transform an enum state, say), swap a cheap
placeholder in with mem::take (uses Default) or mem::replace, instead of cloning:
use std::mem;
enum Draft {
Editing { text: String },
Submitted { text: String, reviewer: String },
}
impl Draft {
fn submit(&mut self, reviewer: &str) {
if let Draft::Editing { text } = self {
let text = mem::take(text); // move the String out, leave "" behind
*self = Draft::Submitted { text, reviewer: reviewer.into() };
}
}
}
fn main() {
let mut d = Draft::Editing { text: "A great read.".into() };
d.submit("librarian");
if let Draft::Submitted { text, reviewer } = &d {
println!("{reviewer}: {text}");
}
}
Other idioms in brief
-
Option as an iterator:
OptionimplementsIntoIterator, sovec.extend(maybe_item)orchain(opt)handle theSome/Nonecases without a branch. -
Temporary mutability: build data with a
mutbinding, then shadow it immutably (let data = data;) or build it in a block expression. -
Return the consumed argument on error: a function that takes ownership and may fail returns the value inside the error (
Errvalue, reason), so callers can retry without having cloned it — asString::from_utf8does withFromUtf8Error::into_bytes. -
Finalisation in destructors: use RAII guards instead of
finally(see Drop, RAII & Drop Check). -
Concatenate strings with
format!when it is clearer than repeatedpush_str.
Design patterns
Builder
Rust has no overloading or default arguments, so types with many optional settings get a builder: a separate type with
chained setters and a final build() that validates. Builders can take self by value (chaining moves the builder) or
&mut self (reusable); by-value is the more common choice.
#[derive(Debug)]
pub struct SearchQuery {
text: String,
limit: usize,
include_lent: bool,
sort_by_year: bool,
}
#[derive(Default)]
pub struct SearchQueryBuilder {
text: Option<String>,
limit: Option<usize>,
include_lent: bool,
sort_by_year: bool,
}
impl SearchQueryBuilder {
pub fn text(mut self, text: impl Into<String>) -> Self {
self.text = Some(text.into());
self
}
pub fn limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
pub fn include_lent(mut self, yes: bool) -> Self {
self.include_lent = yes;
self
}
pub fn sort_by_year(mut self) -> Self {
self.sort_by_year = true;
self
}
pub fn build(self) -> Result<SearchQuery, String> {
let text = self.text.filter(|t| !t.trim().is_empty()).ok_or("text is required")?;
let limit = self.limit.unwrap_or(20);
if limit == 0 || limit > 100 {
return Err(format!("limit must be 1-100, got {limit}"));
}
Ok(SearchQuery { text, limit, include_lent: self.include_lent, sort_by_year: self.sort_by_year })
}
}
impl SearchQuery {
pub fn builder() -> SearchQueryBuilder {
SearchQueryBuilder::default()
}
}
fn main() {
let q = SearchQuery::builder().text("dune").limit(10).sort_by_year().build();
println!("{q:?}");
println!("{:?}", SearchQuery::builder().limit(500).text("x").build());
}
Source: Rust Design Patterns: Builder
For many builders, a derive macro (derive_builder, bon, or the small one on
Macros) removes the boilerplate. When illegal combinations should be
compile errors rather than build() errors, combine the builder with typestate.
Newtype
Wrap a type in a single-field tuple struct to give it a distinct type, attach invariants and methods, hide its
representation, or implement foreign traits for it. shelf-core's Isbn(String) guarantees a valid check digit; a
MemberId(u32) cannot be passed where a BookId(u32) is expected. See
Structs & Methods.
RAII guards
Tie an action to a value’s lifetime: acquire in the constructor, release in Drop. MutexGuard, File, database
transactions and the Rollback guard on Drop, RAII & Drop
Check are all instances.
Strategy with traits or closures
Varying an algorithm is done with a trait (static dispatch through generics, or Box<dyn Trait> when chosen at run time)
or simply with a closure parameter — often the lightest option:
trait FeePolicy {
fn fee_cents(&self, days_late: u32) -> u32;
}
struct Standard;
struct Student;
impl FeePolicy for Standard {
fn fee_cents(&self, days_late: u32) -> u32 {
days_late.min(10) * 50
}
}
impl FeePolicy for Student {
fn fee_cents(&self, days_late: u32) -> u32 {
days_late.saturating_sub(2).min(10) * 25
}
}
fn invoice(policy: &dyn FeePolicy, days: u32) -> u32 { // strategy chosen at run time
policy.fee_cents(days)
}
fn invoice_with(days: u32, fee: impl Fn(u32) -> u32) -> u32 { // strategy as a closure
fee(days)
}
fn main() {
let policies: [(&str, Box<dyn FeePolicy>); 2] = [("standard", Box::new(Standard)), ("student", Box::new(Student))];
for (name, p) in &policies {
println!("{name}: {}", invoice(p.as_ref(), 5));
}
println!("holiday: {}", invoice_with(5, |_| 0));
}
Source: Rust Design Patterns: Strategy
Typestate
Encode an object’s state in its type parameter so that invalid transitions do not compile (ImportJob<Configured> has
no run; ImportJob<Validated> does). Shown on
Generics & Const Generics.
Command
Represent operations as values — usually an enum — so they can be queued, logged, undone or sent to another task. In Rust
the enum form is idiomatic, and an actor task that owns state and processes a Command channel is the async version
(see Async Runtimes & Tokio):
#[derive(Debug, Clone)]
enum Command {
Lend { isbn: String, member: String },
Return { isbn: String },
}
#[derive(Default)]
struct Desk {
lent: Vec<(String, String)>,
history: Vec<Command>,
}
impl Desk {
fn execute(&mut self, cmd: Command) {
match &cmd {
Command::Lend { isbn, member } => self.lent.push((isbn.clone(), member.clone())),
Command::Return { isbn } => self.lent.retain(|(i, _)| i != isbn),
}
self.history.push(cmd); // commands are data: easy to log, replay or undo
}
}
fn main() {
let mut desk = Desk::default();
desk.execute(Command::Lend { isbn: "978".into(), member: "alice".into() });
desk.execute(Command::Return { isbn: "978".into() });
println!("{} on loan, {} commands logged", desk.lent.len(), desk.history.len());
}
Source: Rust Design Patterns: Command
Visitor
For operations over a tree of heterogeneous nodes (an AST, a document), a visitor trait with one method per node kind
separates traversal from the operation. With a closed set of node types, a match over an enum is usually simpler; the
visitor pattern shines when users of a library add operations (serde’s Visitor is a well-known example).
Source: Rust Design Patterns: Visitor
Anti-patterns
-
Cloning to satisfy the borrow checker. Sprinkling
.clone()until errors disappear hides an ownership design problem and costs allocations. Restructure: shorten borrows, split structs, pass indices, or share withRc/Arcdeliberately. (Cloning is fine when you genuinely need a second copy, and in prototypes.) -
#![deny(warnings)]in source. A new compiler release can add warnings and break the build for everyone downstream; deny warnings in CI with-D warningsinstead (see Tooling). -
Deref polymorphism. Implementing
Derefon a struct to "inherit" another type’s methods emulates inheritance badly: methods appear implicitly, traits are not inherited, and readers are surprised. Delegate explicitly or useAsRef.
The Rust API Guidelines
The official Rust API Guidelines are a checklist for public library APIs, grouped by theme. The highlights:
| Theme | Selected guidelines |
|---|---|
Naming (C-CASE, C-CONV, C-GETTER, C-ITER) |
RFC 430 casing; |
Interoperability (C-COMMON-TRAITS, C-CONV-TRAITS, C-SEND-SYNC, C-GOOD-ERR) |
eagerly implement |
Documentation (C-CRATE-DOC, C-EXAMPLE, C-FAILURE) |
crate-level docs with examples; every item has an example; document errors, panics and safety |
Predictability (C-CTOR, C-METHOD, C-SMART-PTR) |
constructors are static inherent methods; functions with a clear receiver are methods; smart pointers do not add inherent methods |
Flexibility (C-CALLER-CONTROL, C-GENERIC, C-OBJECT) |
let callers decide where to copy and allocate; minimise assumptions with generics; traits are object-safe (dyn-compatible) if they may be used as trait objects |
Type safety (C-NEWTYPE, C-CUSTOM-TYPE, C-BITFLAG, C-BUILDER) |
newtypes for static distinctions; custom types instead of
|
Dependability (C-VALIDATE, C-DTOR-FAIL, C-DTOR-BLOCK) |
validate arguments; destructors never fail and do not block |
Future proofing (C-SEALED, C-STRUCT-PRIVATE, C-STRUCT-BOUNDS) |
sealed traits; private fields; no trait bounds on struct definitions that would leak into every user |
Source: Rust API Guidelines: Checklist
A small example of "custom types instead of bool arguments": catalog.search(query, true, false) tells the reader
nothing; catalog.search(query, Availability::IncludeLent, Order::ByTitle) does.
Rust for object-oriented developers
The Book’s chapter 18 asks whether Rust is object-oriented. The mapping for developers coming from Java, C# or C++:
| OOP concept | Rust equivalent |
|---|---|
class with fields and methods |
|
encapsulation |
module privacy: private fields, public methods |
interface |
trait |
inheritance of behaviour |
default methods in traits; composition and delegation |
inheritance of data |
composition: embed a struct as a field |
polymorphism |
generics with trait bounds (static) or |
abstract class |
trait with required and default methods |
constructor / destructor |
|
null object |
|
exceptions |
|
singleton |
pass state explicitly; |
The Book implements its blog-post workflow twice — with trait-object states (the classic State pattern) and with distinct types per state (typestate) — and concludes that the second, which makes invalid states unrepresentable, plays to Rust’s strengths.
References
-
Rust API Guidelines and its checklist
-
Rust Design Patterns (community): idioms, design patterns, anti-patterns
-
The Rust Programming Language: Object-Oriented Programming Features
-
The Rust Programming Language: Implementing an Object-Oriented Design Pattern