Rust Learning Guide - Anti-Patterns
Overview
The antipatterns.rs module
Source Code File
demonstrates 15 common Rust mistakes and their correct solutions. I put this together because I noticed the same patterns coming up, especially from developers (myself included) fighting the borrow checker instead of working with it.
Anti-Patterns Covered
1. Unnecessary .clone() Everywhere
- ❌ Cloning to avoid borrow checker
- ✅ Use references (&T) instead
2. Using .unwrap() in Production
- ❌ Crashes program on error
- ✅ Return Result<T, E> and handle gracefully
3. Returning References to Local Variables
- ❌ Dangling references (won’t compile)
- ✅ Return owned data or use ‘static lifetime
4. Using String When &str Would Work
- ❌ Forces caller to own String
- ✅ Accept &str - works with both
5. Ignoring Compiler Warnings
- ❌ Unused mut, unused variables
- ✅ Listen to the compiler
6. Using Indices Instead of Iterators
- ❌ C-style loops can panic
- ✅ Use iterators - safer and clearer
7. Manually Implementing What Traits Provide
- ❌ Manual equality checks
- ✅ #[derive(PartialEq, Debug, Clone)]
8. Using Vec When Array Would Work
- ❌ Heap allocation for fixed-size data
- ✅ Use arrays [T; N] for stack allocation
9. Nested match/if let Instead of Combinators
- ❌ Deeply nested matching
- ✅ Use .map(), .filter(), .and_then()
10. Using Mutex When Not Needed
- ❌ Mutex in single-threaded code
- ✅ Just use regular variables
11. Collecting Iterator Just to Iterate Again
- ❌ Unnecessary intermediate collection
- ✅ Chain iterators directly
12. Using Deref Coercion as Inheritance
- ❌ Simulating inheritance with Deref
- ✅ Use composition explicitly
13. Panicking in Library Code
- ❌ panic!() crashes caller’s program
- ✅ Return Result and let caller decide
14. Using format! When to_string() Works
- ❌ Overkill for simple conversions
- ✅ Use .to_string() for clarity
15. Implementing Default Manually
- ❌ Manual new() constructor
- ✅ Implement Default trait
Key Principle
Work WITH Rust’s ownership system, not against it.
Here’s what clicked for me after working through these: if you’re fighting the borrow checker, you’re almost certainly doing it wrong. The compiler is genuinely trying to help you write safe, efficient code, and once you stop resisting and start listening, the patterns become second nature.
Usage
Run the program and select option 5 to see the anti-patterns summary.
// Rust Anti-Patterns - Common Mistakes and How to Fix Them
// This module demonstrates what NOT to do and shows the correct approach
#![allow(dead_code, unused_variables, unused_mut)]
// ============================================================================
// ANTI-PATTERN 1: Unnecessary .clone() Everywhere
// ============================================================================
// ❌ WRONG: Cloning to avoid fighting the borrow checker
fn antipattern_clone_spam() {
let data = vec![1, 2, 3, 4, 5];
// Cloning unnecessarily - expensive and defeats Rust's zero-cost abstractions
let sum = calculate_sum(data.clone());
let max = find_max(data.clone());
let min = find_min(data.clone());
fn calculate_sum(v: Vec<i32>) -> i32 { v.iter().sum() }
fn find_max(v: Vec<i32>) -> i32 { *v.iter().max().unwrap() }
fn find_min(v: Vec<i32>) -> i32 { *v.iter().min().unwrap() }
}
// ✅ CORRECT: Use references instead of cloning
fn correct_use_references() {
let data = vec![1, 2, 3, 4, 5];
// Borrow instead of clone - zero cost, no memory allocation
let sum = calculate_sum(&data);
let max = find_max(&data);
let min = find_min(&data);
fn calculate_sum(v: &[i32]) -> i32 { v.iter().sum() }
fn find_max(v: &[i32]) -> i32 { *v.iter().max().unwrap() }
fn find_min(v: &[i32]) -> i32 { *v.iter().min().unwrap() }
}
// ============================================================================
// ANTI-PATTERN 2: Using .unwrap() in Production Code
// ============================================================================
// ❌ WRONG: unwrap() causes panics - crashes your program
fn antipattern_unwrap_everywhere(filename: &str) -> String {
use std::fs;
// If file doesn't exist or can't be read, program crashes!
let content = fs::read_to_string(filename).unwrap();
content
}
// ✅ CORRECT: Properly handle errors with Result
fn correct_error_handling(filename: &str) -> Result<String, std::io::Error> {
use std::fs;
// Return the Result - let caller decide how to handle error
fs::read_to_string(filename)
}
// Or handle the error gracefully
fn correct_error_handling_with_default(filename: &str) -> String {
use std::fs;
// Provide fallback behavior instead of crashing
fs::read_to_string(filename).unwrap_or_else(|_| String::from("default content"))
}
// ============================================================================
// ANTI-PATTERN 3: Returning References to Local Variables
// ============================================================================
// ❌ WRONG: This won't compile - dangling reference
// fn antipattern_return_local_reference() -> &str {
// let s = String::from("hello");
// &s // ERROR: s is dropped at end of function, reference would be invalid
// }
// ✅ CORRECT: Return owned data
fn correct_return_owned() -> String {
let s = String::from("hello");
s // Move ownership to caller
}
// Or use static lifetime for constants
fn correct_return_static() -> &'static str {
"hello" // String literals have 'static lifetime
}
// ============================================================================
// ANTI-PATTERN 4: Using String When &str Would Work
// ============================================================================
// ❌ WRONG: Forces caller to own a String
fn antipattern_require_string(name: String) {
println!("Hello, {}", name);
}
// ✅ CORRECT: Accept &str - works with both String and &str
fn correct_accept_str_slice(name: &str) {
println!("Hello, {}", name);
}
fn demonstrate_string_flexibility() {
let owned = String::from("Alice");
let borrowed = "Bob";
// correct_accept_str_slice works with both
correct_accept_str_slice(&owned);
correct_accept_str_slice(borrowed);
// antipattern_require_string forces conversion
// antipattern_require_string(borrowed.to_string()); // Unnecessary allocation
}
// ============================================================================
// ANTI-PATTERN 5: Ignoring Compiler Warnings
// ============================================================================
// ❌ WRONG: Unused mut, unused variables
fn antipattern_ignore_warnings() {
let mut x = 5; // Warning: variable does not need to be mutable
let y = 10; // Warning: unused variable
println!("{}", x);
}
// ✅ CORRECT: Listen to the compiler
fn correct_heed_warnings() {
let x = 5; // Removed unnecessary mut
// Removed unused variable
println!("{}", x);
}
// ============================================================================
// ANTI-PATTERN 6: Using Indices Instead of Iterators
// ============================================================================
// ❌ WRONG: C-style index loops - verbose and can panic
fn antipattern_index_loops(numbers: &[i32]) -> i32 {
let mut sum = 0;
for i in 0..numbers.len() {
sum += numbers[i]; // Can panic if index out of bounds
}
sum
}
// ✅ CORRECT: Use iterators - safer and more idiomatic
fn correct_use_iterators(numbers: &[i32]) -> i32 {
numbers.iter().sum() // Clear, concise, can't panic
}
// ============================================================================
// ANTI-PATTERN 7: Manually Implementing What Traits Provide
// ============================================================================
// ❌ WRONG: Manual equality check
#[derive(Clone)]
struct Person {
name: String,
age: u32,
}
fn antipattern_manual_equality(p1: &Person, p2: &Person) -> bool {
p1.name == p2.name && p1.age == p2.age
}
// ✅ CORRECT: Derive traits
#[derive(Clone, PartialEq, Eq, Debug)]
struct PersonCorrect {
name: String,
age: u32,
}
fn correct_use_traits() {
let p1 = PersonCorrect { name: "Alice".to_string(), age: 30 };
let p2 = PersonCorrect { name: "Alice".to_string(), age: 30 };
// Just use == operator
println!("Equal: {}", p1 == p2);
}
// ============================================================================
// ANTI-PATTERN 8: Using Vec When Array Would Work
// ============================================================================
// ❌ WRONG: Heap allocation for fixed-size data
fn antipattern_vec_for_fixed_size() {
let days = vec!["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
// Unnecessary heap allocation
}
// ✅ CORRECT: Use array for compile-time known sizes
fn correct_use_array() {
let days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
// Stack allocated, no runtime overhead
}
// ============================================================================
// ANTI-PATTERN 9: Nested match/if let Instead of Combinators
// ============================================================================
// ❌ WRONG: Deeply nested matching
fn antipattern_nested_matching(input: Option<String>) -> Option<usize> {
match input {
Some(s) => {
if s.len() > 0 {
Some(s.len())
} else {
None
}
}
None => None,
}
}
// ✅ CORRECT: Use combinators
fn correct_use_combinators(input: Option<String>) -> Option<usize> {
input.filter(|s| !s.is_empty()).map(|s| s.len())
}
// ============================================================================
// ANTI-PATTERN 10: Using Mutex When Not Needed
// ============================================================================
use std::sync::Mutex;
// ❌ WRONG: Mutex in single-threaded code
fn antipattern_unnecessary_mutex() {
let counter = Mutex::new(0);
for _ in 0..10 {
let mut num = counter.lock().unwrap();
*num += 1;
}
// Unnecessary overhead - no threading here
}
// ✅ CORRECT: Just use a regular variable
fn correct_simple_variable() {
let mut counter = 0;
for _ in 0..10 {
counter += 1;
}
}
// ============================================================================
// ANTI-PATTERN 11: Collecting Iterator Just to Iterate Again
// ============================================================================
// ❌ WRONG: Unnecessary intermediate collection
fn antipattern_collect_then_iterate(numbers: &[i32]) -> i32 {
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
doubled.iter().sum() // Why collect if we just iterate again?
}
// ✅ CORRECT: Chain iterators
fn correct_chain_iterators(numbers: &[i32]) -> i32 {
numbers.iter().map(|x| x * 2).sum() // No intermediate allocation
}
// ============================================================================
// ANTI-PATTERN 12: Using Deref Coercion as Inheritance
// ============================================================================
use std::ops::Deref;
// ❌ WRONG: Trying to simulate inheritance
struct Employee {
person: Person,
employee_id: u32,
}
impl Deref for Employee {
type Target = Person;
fn deref(&self) -> &Self::Target {
&self.person
}
}
// This is confusing - Deref is for smart pointers, not inheritance
// ✅ CORRECT: Use composition explicitly
struct EmployeeCorrect {
person: PersonCorrect,
employee_id: u32,
}
impl EmployeeCorrect {
fn person(&self) -> &PersonCorrect {
&self.person
}
}
// ============================================================================
// ANTI-PATTERN 13: Panicking in Library Code
// ============================================================================
// ❌ WRONG: Library function that panics
fn antipattern_library_panic(divisor: i32) -> i32 {
if divisor == 0 {
panic!("Cannot divide by zero!"); // Crashes caller's program
}
100 / divisor
}
// ✅ CORRECT: Return Result and let caller decide
fn correct_library_result(divisor: i32) -> Result<i32, String> {
if divisor == 0 {
Err("Cannot divide by zero".to_string())
} else {
Ok(100 / divisor)
}
}
// ============================================================================
// ANTI-PATTERN 14: Using format! When to_string() Works
// ============================================================================
// ❌ WRONG: Unnecessary formatting
fn antipattern_format_simple() {
let num = 42;
let s = format!("{}", num); // Overkill for simple conversion
}
// ✅ CORRECT: Use to_string() for simple cases
fn correct_to_string() {
let num = 42;
let s = num.to_string(); // Clearer intent
}
// ============================================================================
// ANTI-PATTERN 15: Implementing Default Manually
// ============================================================================
// ❌ WRONG: Manual default implementation
struct Config {
timeout: u32,
retries: u32,
}
impl Config {
fn new() -> Self {
Config {
timeout: 30,
retries: 3,
}
}
}
// ✅ CORRECT: Implement Default trait
struct ConfigCorrect {
timeout: u32,
retries: u32,
}
impl Default for ConfigCorrect {
fn default() -> Self {
ConfigCorrect {
timeout: 0,
retries: 0,
}
}
}
// Or with custom defaults
struct ConfigCustom {
timeout: u32,
retries: u32,
}
impl Default for ConfigCustom {
fn default() -> Self {
ConfigCustom {
timeout: 30,
retries: 3,
}
}
}
// ============================================================================
// DEMO FUNCTION
// ============================================================================
pub fn run_antipatterns_demo() {
println!("=== Rust Anti-Patterns Demo ===\n");
println!("1. Clone vs References:");
println!(" ❌ Cloning everywhere wastes memory and CPU");
println!(" ✅ Use references (&T) when you don't need ownership\n");
println!("2. Error Handling:");
println!(" ❌ .unwrap() crashes your program");
println!(" ✅ Return Result<T, E> and handle errors gracefully\n");
println!("3. String Types:");
println!(" ❌ Requiring String forces unnecessary allocations");
println!(" ✅ Accept &str - works with both String and string literals\n");
println!("4. Loops:");
println!(" ❌ Index-based loops can panic and are verbose");
println!(" ✅ Use iterators - safer, clearer, often faster\n");
println!("5. Collections:");
println!(" ❌ Vec for fixed-size data wastes heap memory");
println!(" ✅ Use arrays [T; N] for compile-time known sizes\n");
println!("6. Iterators:");
println!(" ❌ Collecting just to iterate again wastes memory");
println!(" ✅ Chain iterator operations - lazy evaluation\n");
println!("7. Library Design:");
println!(" ❌ panic!() in libraries crashes user's program");
println!(" ✅ Return Result and let caller handle errors\n");
println!("8. Traits:");
println!(" ❌ Manual implementations of equality, debug, etc.");
println!(" ✅ #[derive(Debug, PartialEq, Clone)] - let compiler do it\n");
println!("Key Principle: Work WITH Rust's ownership system, not against it.");
println!("If you're fighting the borrow checker, you're probably doing it wrong.\n");
}