rust-design-patterns

Idioms and patterns for writing idiomatic Rust code. Focus on Rust-specific patterns that leverage ownership, borrowing, and the type system.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "rust-design-patterns" with this command: npx skills add ahonn/dotfiles/ahonn-dotfiles-rust-design-patterns

Rust Design Patterns

Idioms and patterns for writing idiomatic Rust code. Focus on Rust-specific patterns that leverage ownership, borrowing, and the type system.

Decision Tree

Problem? ├── Borrow checker error? │ ├── Need to move value from &mut enum → mem::take/replace │ ├── Need independent field borrows → struct decomposition │ ├── Tempted to clone? → Check: Rc/Arc? Or refactor ownership? │ └── Lifetime too short → consider owned types or 'static │ ├── API design? │ ├── Many constructor params → Builder pattern │ ├── Accept flexible input → Borrowed types (&str, &[T]) │ ├── Type safety at compile time → Newtype pattern │ ├── Default values needed → Default trait + struct update syntax │ └── Resource cleanup needed → RAII guards (Drop trait) │ ├── FFI boundary? │ ├── Error handling → Integer codes + error description fn │ ├── String passing → CString/CStr patterns │ └── Object lifetime → Opaque pointers with explicit free │ ├── Unsafe code? │ ├── Need unsafe operations → Contain in small modules with safe wrappers │ └── FFI types → Type consolidation into opaque wrappers │ └── Performance concern? ├── Avoid monomorphization bloat → On-stack dynamic dispatch └── Reduce allocations → mem::take instead of clone

Quick Patterns

Borrowed Types (CRITICAL)

Prefer &str over &String , &[T] over &Vec<T> :

// Bad: only accepts &String fn process(s: &String) { }

// Good: accepts &String, &str, string literals fn process(s: &str) { }

// Usage: all work with &str process(&my_string); // String process("literal"); // &'static str process(&my_string[1..5]); // slice

Why: Deref coercion allows &String → &str , but not reverse. Using borrowed types accepts more input types.

mem::take Pattern (CRITICAL)

Move owned values out of &mut without clone:

use std::mem;

enum State { Active { data: String }, Inactive, }

fn deactivate(state: &mut State) { if let State::Active { data } = state { // Take ownership without clone let owned_data = mem::take(data); *state = State::Inactive; // use owned_data... } }

When: Changing enum variants while keeping owned inner data. Avoids clone anti-pattern.

Newtype Pattern

Type safety with zero runtime cost:

// Distinct types prevent mixing struct UserId(u64); struct OrderId(u64);

fn get_order(user: UserId, order: OrderId) { }

// Compile error: can't mix up IDs // get_order(order_id, user_id);

When: Need compile-time distinction between same underlying types, or custom trait implementations.

Builder Pattern

For complex construction:

#[derive(Default)] struct RequestBuilder { url: String, timeout: Option<u32>, headers: Vec<(String, String)>, }

impl RequestBuilder { fn url(mut self, url: impl Into<String>) -> Self { self.url = url.into(); self }

fn timeout(mut self, ms: u32) -> Self {
    self.timeout = Some(ms);
    self
}

fn build(self) -> Request {
    Request { /* ... */ }
}

}

// Usage let req = RequestBuilder::default() .url("https://example.com") .timeout(5000) .build();

When: Many optional parameters, or construction has validation/side effects.

RAII Guards

Resource management through ownership:

struct FileGuard { file: File, }

impl Drop for FileGuard { fn drop(&mut self) { // Cleanup runs automatically when guard goes out of scope self.file.sync_all().ok(); } }

fn process() -> Result<()> { let guard = FileGuard { file: File::open("data.txt")? }; // Even with early return or panic, Drop runs do_work()?; Ok(()) } // guard.drop() called here

When: Need guaranteed cleanup (locks, files, connections, transactions).

Default + Struct Update

Partial initialization with defaults:

#[derive(Default)] struct Config { host: String, port: u16, timeout: u32, retries: u8, }

let config = Config { host: "localhost".into(), port: 8080, ..Default::default() // timeout=0, retries=0 };

On-Stack Dynamic Dispatch

Avoid heap allocation for trait objects:

use std::io::{self, Read};

fn process(use_stdin: bool) -> io::Result<String> { let readable: &mut dyn Read = if use_stdin { &mut io::stdin() } else { &mut std::fs::File::open("input.txt")? };

let mut buf = String::new();
readable.read_to_string(&#x26;mut buf)?;
Ok(buf)

}

When: Need dynamic dispatch without Box allocation. Since Rust 1.79, lifetime extension makes this ergonomic.

Option as Iterator

Option implements IntoIterator (0 or 1 element):

let maybe_name = Some("Turing"); let mut names = vec!["Curry", "Kleene"];

// Extend with Option names.extend(maybe_name);

// Chain with Option for name in names.iter().chain(maybe_name.iter()) { println!("{name}"); }

Tip: For always-Some , prefer std::iter::once(value) .

Closure Capture Control

Control what closures capture via rebinding:

use std::rc::Rc;

let num1 = Rc::new(1); let num2 = Rc::new(2);

let closure = { let num2 = num2.clone(); // clone before move let num1 = num1.as_ref(); // borrow move || { *num1 + *num2 } }; // num1 still usable, num2 was cloned

Temporary Mutability

Make variable immutable after setup:

// Method 1: Nested block let data = { let mut data = get_vec(); data.sort(); data }; // data is immutable here

// Method 2: Rebinding let mut data = get_vec(); data.sort(); let data = data; // now immutable

Return Consumed Argument on Error

If function consumes argument, return it in error for retry:

pub struct SendError(pub String); // contains the original value

pub fn send(value: String) -> Result<(), SendError> { if can_send() { do_send(&value); Ok(()) } else { Err(SendError(value)) // caller can retry } }

// Usage: retry loop without clone let mut msg = "hello".to_string(); loop { match send(msg) { Ok(()) => break, Err(SendError(m)) => { msg = m; } // recover and retry } }

Example: String::from_utf8 returns FromUtf8Error containing original Vec<u8> .

Anti-Patterns Checklist

Review code for these common mistakes:

Clone to satisfy borrow checker - Usually indicates ownership design issue. Consider mem::take , Rc /Arc , or refactoring.

#![deny(warnings)] in library - Breaks downstream on new Rust versions. Use RUSTFLAGS="-D warnings" in CI instead.

Deref for inheritance - Surprising behavior, doesn't provide true subtyping. Use composition + delegation or traits.

&String or &Vec<T> in function params - Use &str or &[T] for flexibility.

Manual drop() calls - Usually unnecessary. If needed for ordering, prefer scoped blocks.

Ignoring clippy suggestions for .clone()

  • Run cargo clippy to find unnecessary clones.

References

For detailed patterns with full examples:

  • ownership-patterns.md - Borrow checker patterns: mem::take/replace, struct decomposition, RAII guards, Rc/Arc decisions

  • api-design.md - API patterns: borrowed types, builders, newtype, Default trait, FFI

  • common-pitfalls.md - Anti-patterns in detail: clone abuse, deny(warnings), Deref polymorphism

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

plan-code-workflow

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

code-quality

No summary provided by upstream source.

Repository SourceNeeds Review
General

seo-backlink-strategy

No summary provided by upstream source.

Repository SourceNeeds Review
General

react-best-practices

No summary provided by upstream source.

Repository SourceNeeds Review