Chapter 1: Foundation Patterns
This section covers fundamental design patterns that demonstrate core Rust concepts. Each pattern introduces new Rust features progressively, building your understanding of how Rust's ownership system, type safety, and zero-cost abstractions enable safer and more efficient implementations of traditional design patterns. After reading Chapter 1 you can write basic idiomatic Rust.
Patterns Covered
NewType Pattern
Wrapping existing types for type safety and semantic clarity
Core Rust concepts introduced:
- Tuple Structs and the NewType Pattern
- Ownership and Borrowing
- String Types (
Stringvs&str) - Derive Macros (
Debug,Clone,Copy, etc.) - Result Type and Error Handling
- Trait Implementation
- Associated Functions vs Methods
- Pattern Matching and Option Type
- Visibility and Encapsulation
- Type Safety Without Runtime Cost
Builder Pattern
Constructing complex objects step by step with fluent interfaces
New Rust concepts introduced:
- Move Semantics in Method Chaining
- Trait Bounds and Generic Programming (
impl Into<String>) - PhantomData and Type-State Pattern
- Type-Level Programming
- Compile-Time Guarantees vs Runtime Checks
- Use-After-Move Prevention
- Zero-Cost State Machines
Iterator Pattern
Accessing elements sequentially with functional programming features
New Rust concepts introduced:
- Lifetimes and Lifetime Parameters
- Mutable References and Borrowing Rules
- Closure Syntax and Capture
- Iterator Combinators and Lazy Evaluation
- Type Inference and the Turbofish (
::<Type>) - Slice Types and Fat Pointers
- Consuming vs Borrowing Iteration
- Associated Types in Traits
- Infinite Iterators and State
Result/Error Handling Pattern
Managing errors explicitly through the type system
New Rust concepts introduced:
- Enums with Data and Pattern Matching
- The
?Operator and Error Propagation - From Trait and Automatic Error Conversion
- Result Combinators and Functional Error Handling
- Error Display and Debug Traits
- Unit Structs for Namespacing
- Advanced Error Handling Patterns
- Testing Error Conditions
- Errors as Values Philosophy
RAII/Drop Pattern
Automatic resource management through scope-based cleanup
New Rust concepts introduced:
- The Drop Trait and Automatic Cleanup
- Scope-Based Resource Management (RAII)
- Consuming Methods and Resource Transfer
- Advanced Closure Patterns (
FnOnce,FnMut,Fn) - Pattern Matching with
Option::take() - Disarming Pattern
- Unit Types and Zero-Sized Types
- Generic Type Parameters with Trait Bounds
- Resource Management Patterns
- Testing Automatic Behavior
Learning Approach
Each pattern chapter follows this structure:
- Core Concept - What the pattern does and why it's useful
- Java Implementation - Traditional object-oriented approach
- Rust Implementation - How Rust improves upon the pattern
- Key Differences - Comparison highlighting Rust's advantages
- Core Rust Concepts Introduced - New language features demonstrated
By the end of these foundation patterns, you'll have a solid understanding of Rust's core concepts and be ready to tackle more advanced design patterns and architectural techniques.
NewType Pattern
The NewType pattern isn't one of the classic Gang of Four design patterns, but it's a fundamental Rust idiom that provides type safety and semantic clarity. It's particularly powerful in Rust due to the language's zero-cost abstractions and strong type system.
What is the NewType Pattern?
The NewType pattern involves wrapping an existing type in a new struct to create a distinct type with the same underlying representation but different semantics. This provides:
- Type Safety: Prevents mixing up semantically different values of the same underlying type
- API Control: You can implement only the traits and methods you want to expose
- Zero Runtime Cost: The wrapper is eliminated at compile time
- Clear Intent: Makes code more self-documenting
Comparison with Java
In Java, you might use wrapper classes, but they have runtime overhead:
// Java - has runtime overhead
public class UserId {
private final int value;
public UserId(int value) {
this.value = value;
}
public int getValue() { return value; }
}
In Rust, the NewType pattern has zero runtime cost due to the compiler's optimizations.
Rust Implementation
use std::fmt; // NewType pattern - wrapping primitive types for type safety #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct UserId(u32); #[derive(Debug, Clone, PartialEq)] pub struct Email(String); // Implementing methods for NewType impl UserId { pub fn new(id: u32) -> Self { UserId(id) } pub fn as_u32(&self) -> u32 { self.0 } } impl Email { pub fn new(email: String) -> Result<Self, &'static str> { if email.contains('@') { Ok(Email(email)) } else { Err("Invalid email format") } } pub fn domain(&self) -> Option<&str> { self.0.split('@').nth(1) } pub fn as_str(&self) -> &str { &self.0 } } // Custom Display implementation impl fmt::Display for UserId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "User#{}", self.0) } } impl fmt::Display for Email { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } // Example usage demonstrating type safety pub struct User { id: UserId, email: Email, } impl User { pub fn new(id: u32, email: String) -> Result<Self, &'static str> { Ok(User { id: UserId::new(id), email: Email::new(email)?, }) } pub fn id(&self) -> UserId { self.id } pub fn email(&self) -> &Email { &self.email } } fn main() { // Create some NewType instances let user_id = UserId::new(123); let email = Email::new("user@example.com".to_string()).unwrap(); println!("User ID: {}", user_id); println!("Email domain: {:?}", email.domain()); // Create user let user = User::new(123, "alice@example.com".to_string()).unwrap(); // Demonstrate that NewTypes prevent accidental mixing let another_user_id = UserId::new(789); println!("User IDs can be compared: {}", user_id == another_user_id); println!("User ID as u32: {}", user_id.as_u32()); }
More Complex NewType Example
A more sophisticated example showing validation and behavior:
use std::fmt; // Example: A more complex NewType with validation and behavior #[derive(Debug, Clone, PartialEq)] pub struct Temperature(f64); impl Temperature { pub fn celsius(temp: f64) -> Self { Temperature(temp) } pub fn fahrenheit(temp: f64) -> Self { Temperature((temp - 32.0) * 5.0 / 9.0) } pub fn to_celsius(&self) -> f64 { self.0 } pub fn to_fahrenheit(&self) -> f64 { self.0 * 9.0 / 5.0 + 32.0 } pub fn to_kelvin(&self) -> f64 { self.0 + 273.15 } } impl fmt::Display for Temperature { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:.1}°C", self.0) } } fn main() { // Temperature example let temp_c = Temperature::celsius(25.0); let temp_f = Temperature::fahrenheit(77.0); println!("Temperature in Celsius: {}", temp_c); println!("Temperature in Fahrenheit: {:.1}°F", temp_c.to_fahrenheit()); println!("Temperature in Kelvin: {:.1}K", temp_c.to_kelvin()); }
Key Benefits Demonstrated
-
Type Safety:
UserIdtypes can't be accidentally swapped with other wrapped types, even though they both wrapu32 -
Zero Cost: The wrapper types are compiled away - no runtime overhead
-
Controlled API: You decide which operations are available (notice we don't derive
Addfor IDs) -
Validation: The
Emailtype enforces basic validation at construction time -
Semantic Clarity:
Temperature::celsius(25.0)is much clearer than just25.0
Advanced NewType Patterns
You can also implement traits selectively:
#![allow(unused)] fn main() { // Only implement specific traits you want impl std::ops::Add for Temperature { type Output = Temperature; fn add(self, other: Temperature) -> Temperature { Temperature(self.0 + other.0) } } }
Relation to Gang of Four
While not a GoF pattern, NewType often works with other patterns:
- Facade: NewTypes can simplify complex APIs
- Adapter: Wrap foreign types to match your interface
- Decorator: Add behavior to existing types without inheritance
The NewType pattern is particularly powerful in Rust because it leverages the type system for safety without runtime cost, something that's harder to achieve in classical OOP languages like Java.
Core Rust concepts introduced
1. Tuple Structs and the NewType Pattern
#![allow(unused)] fn main() { pub struct UserId(u32); }
- Tuple structs: Single-field structs with unnamed fields
- NewType pattern: Wrapping existing types for type safety
- Field access: Use
.0to access the wrapped value - Zero-cost abstraction: Wrapper is eliminated at compile time
2. Ownership and Borrowing
#![allow(unused)] fn main() { pub fn as_str(&self) -> &str { &self.0 // Borrowing the inner string data } }
- Ownership: Every value has exactly one owner
- Borrowing: Using
&to create references without taking ownership &self: Immutable borrow of the struct instance&str: String slice (borrowed view of string data)- Memory safety: Prevents use-after-free and data races at compile time
3. String Types
#![allow(unused)] fn main() { pub struct Email(String); // Owned string pub fn as_str(&self) -> &str // String slice (borrowed view) }
String: Owned, heap-allocated, growable string&str: Immutable string slice (view into string data)- Automatic conversion:
Stringcan be borrowed as&str - Memory efficiency: Pass
&strinstead ofStringwhen you don't need ownership
4. Derive Macros
#![allow(unused)] fn main() { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct UserId(u32); }
- Automatic trait implementations: Compiler generates code for common traits
Debug: Enables{:?}and{:#?}formatting for debuggingClone: Explicit copying with.clone()methodCopy: Implicit copying for simple stack-allocated typesPartialEq/Eq: Equality comparisons with==and!=Hash: Enables use inHashMap,HashSet, etc.
5. Result Type and Error Handling
#![allow(unused)] fn main() { pub fn new(email: String) -> Result<Self, &'static str> { if email.contains('@') { Ok(Email(email)) } else { Err("Invalid email format") } } }
Result<T, E>: Rust's approach to fallible operations- No exceptions: Errors are values, not thrown exceptions
- Explicit handling: Must handle both
Ok(value)andErr(error)cases - Type safety: Prevents forgotten error handling at compile time
6. Trait Implementation
#![allow(unused)] fn main() { impl fmt::Display for UserId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "User#{}", self.0) } } }
- Traits: Similar to interfaces, define shared behavior
impl Trait for Type: Syntax for implementing traits on types- Method signature:
&selffor methods that borrow the instance - Return types: Traits can specify return types like
fmt::Result
7. Associated Functions vs Methods
#![allow(unused)] fn main() { impl UserId { pub fn new(id: u32) -> Self { // Associated function UserId(id) } pub fn as_u32(&self) -> u32 { // Method self.0 } } }
- Associated functions: Don't take
self, called withType::function() - Methods: Take some form of
self, called withinstance.method() Selftype: Refers to the implementing type (here,UserId)- Constructor pattern:
new()is conventionally an associated function
8. Pattern Matching and Option Type
#![allow(unused)] fn main() { pub fn domain(&self) -> Option<&str> { self.0.split('@').nth(1) // Returns Option<&str> } }
Option<T>: Represents values that might not existSome(value): Contains a valueNone: Represents absence of value- No null pointers: Rust eliminates null pointer exceptions
- Explicit handling: Must handle both
SomeandNonecases
9. Visibility and Encapsulation
#![allow(unused)] fn main() { pub struct UserId(u32); // Public struct, but field is private by default }
pub: Makes items publicly visible- Default privacy: Struct fields are private unless marked
pub - Controlled access: Use methods to provide controlled access to data
- Encapsulation: Hide implementation details while exposing safe interfaces
10. Type Safety Without Runtime Cost
#![allow(unused)] fn main() { // This won't compile - different types despite same underlying data: // assert_eq!(user_id, another_different_type_id); // Compile error! }
- Strong typing: Types that wrap the same data are still distinct
- Compile-time guarantees: Type errors caught before runtime
- Zero-cost abstractions: Safety checks happen at compile time
- Performance: No runtime type checking needed
These fundamental concepts form the foundation for all Rust programming and will appear throughout the remaining pattern chapters. Understanding them here will make the subsequent patterns much clearer.
Builder Pattern
The Builder pattern is a creational design pattern that provides a way to construct complex objects step by step. It's particularly useful when you need to create objects with many optional parameters or when the construction process itself is complex and needs to be separated from the object's representation.
Core Concept
The Builder pattern solves the "telescoping constructor" problem where you'd otherwise need multiple constructors with different parameter combinations. Instead, it provides a fluent interface for building objects incrementally.
Java Implementation (Classical OOP)
Let's start with a traditional Java example:
// Product class
class Computer {
private final String cpu;
private final String ram;
private final String storage;
private final boolean hasGraphicsCard;
private final boolean hasWifi;
private final String operatingSystem;
// Private constructor - can only be called by Builder
private Computer(Builder builder) {
this.cpu = builder.cpu;
this.ram = builder.ram;
this.storage = builder.storage;
this.hasGraphicsCard = builder.hasGraphicsCard;
this.hasWifi = builder.hasWifi;
this.operatingSystem = builder.operatingSystem;
}
// Getters
public String getCpu() { return cpu; }
public String getRam() { return ram; }
public String getStorage() { return storage; }
public boolean hasGraphicsCard() { return hasGraphicsCard; }
public boolean hasWifi() { return hasWifi; }
public String getOperatingSystem() { return operatingSystem; }
@Override
public String toString() {
return String.format(
"Computer{cpu='%s', ram='%s', storage='%s', graphics=%b, wifi=%b, os='%s'}",
cpu, ram, storage, hasGraphicsCard, hasWifi, operatingSystem
);
}
// Static nested Builder class
public static class Builder {
// Required parameters
private final String cpu;
private final String ram;
// Optional parameters with defaults
private String storage = "256GB SSD";
private boolean hasGraphicsCard = false;
private boolean hasWifi = true;
private String operatingSystem = "Windows 11";
// Builder constructor with required parameters
public Builder(String cpu, String ram) {
this.cpu = cpu;
this.ram = ram;
}
// Fluent interface methods
public Builder storage(String storage) {
this.storage = storage;
return this;
}
public Builder withGraphicsCard() {
this.hasGraphicsCard = true;
return this;
}
public Builder withoutWifi() {
this.hasWifi = false;
return this;
}
public Builder operatingSystem(String os) {
this.operatingSystem = os;
return this;
}
// Build method creates the final object
public Computer build() {
return new Computer(this);
}
}
}
// Usage example
public class BuilderPatternDemo {
public static void main(String[] args) {
// Basic computer
Computer basicComputer = new Computer.Builder("Intel i5", "8GB")
.build();
// Gaming computer
Computer gamingComputer = new Computer.Builder("Intel i9", "32GB")
.storage("1TB NVMe SSD")
.withGraphicsCard()
.operatingSystem("Windows 11 Pro")
.build();
// Server computer
Computer serverComputer = new Computer.Builder("AMD Threadripper", "128GB")
.storage("2TB SSD RAID")
.withoutWifi()
.operatingSystem("Ubuntu Server")
.build();
System.out.println(basicComputer);
System.out.println(gamingComputer);
System.out.println(serverComputer);
}
}
Rust Implementation
Now let's see how we implement the Builder pattern in Rust. Rust's approach leverages its ownership system and type safety features:
// Product struct #[derive(Debug, Clone)] pub struct Computer { cpu: String, ram: String, storage: String, has_graphics_card: bool, has_wifi: bool, operating_system: String, } impl Computer { // Associated function to create a new builder pub fn builder(cpu: impl Into<String>, ram: impl Into<String>) -> ComputerBuilder { ComputerBuilder::new(cpu, ram) } // Getters pub fn cpu(&self) -> &str { &self.cpu } pub fn ram(&self) -> &str { &self.ram } pub fn storage(&self) -> &str { &self.storage } pub fn has_graphics_card(&self) -> bool { self.has_graphics_card } pub fn has_wifi(&self) -> bool { self.has_wifi } pub fn operating_system(&self) -> &str { &self.operating_system } } // Builder struct pub struct ComputerBuilder { cpu: String, ram: String, storage: String, has_graphics_card: bool, has_wifi: bool, operating_system: String, } impl ComputerBuilder { // Constructor with required parameters pub fn new(cpu: impl Into<String>, ram: impl Into<String>) -> Self { Self { cpu: cpu.into(), ram: ram.into(), storage: "256GB SSD".to_string(), has_graphics_card: false, has_wifi: true, operating_system: "Windows 11".to_string(), } } // Fluent interface methods - each takes ownership and returns Self pub fn storage(mut self, storage: impl Into<String>) -> Self { self.storage = storage.into(); self } pub fn with_graphics_card(mut self) -> Self { self.has_graphics_card = true; self } pub fn without_wifi(mut self) -> Self { self.has_wifi = false; self } pub fn operating_system(mut self, os: impl Into<String>) -> Self { self.operating_system = os.into(); self } // Build method consumes the builder and returns the final product pub fn build(self) -> Computer { Computer { cpu: self.cpu, ram: self.ram, storage: self.storage, has_graphics_card: self.has_graphics_card, has_wifi: self.has_wifi, operating_system: self.operating_system, } } } // Advanced: Type-state pattern for compile-time validation // This ensures required fields are set at compile time pub struct CpuSet; pub struct RamSet; pub struct NotSet; pub struct TypedComputerBuilder<CpuState = NotSet, RamState = NotSet> { cpu: Option<String>, ram: Option<String>, storage: String, has_graphics_card: bool, has_wifi: bool, operating_system: String, _cpu_state: std::marker::PhantomData<CpuState>, _ram_state: std::marker::PhantomData<RamState>, } impl TypedComputerBuilder<NotSet, NotSet> { pub fn new() -> Self { Self { cpu: None, ram: None, storage: "256GB SSD".to_string(), has_graphics_card: false, has_wifi: true, operating_system: "Windows 11".to_string(), _cpu_state: std::marker::PhantomData, _ram_state: std::marker::PhantomData, } } pub fn cpu(self, cpu: impl Into<String>) -> TypedComputerBuilder<CpuSet, NotSet> { TypedComputerBuilder { cpu: Some(cpu.into()), ram: self.ram, storage: self.storage, has_graphics_card: self.has_graphics_card, has_wifi: self.has_wifi, operating_system: self.operating_system, _cpu_state: std::marker::PhantomData, _ram_state: std::marker::PhantomData, } } } impl TypedComputerBuilder<CpuSet, NotSet> { pub fn ram(self, ram: impl Into<String>) -> TypedComputerBuilder<CpuSet, RamSet> { TypedComputerBuilder { cpu: self.cpu, ram: Some(ram.into()), storage: self.storage, has_graphics_card: self.has_graphics_card, has_wifi: self.has_wifi, operating_system: self.operating_system, _cpu_state: std::marker::PhantomData, _ram_state: std::marker::PhantomData, } } } impl<CpuState, RamState> TypedComputerBuilder<CpuState, RamState> { pub fn storage(mut self, storage: impl Into<String>) -> Self { self.storage = storage.into(); self } pub fn with_graphics_card(mut self) -> Self { self.has_graphics_card = true; self } pub fn without_wifi(mut self) -> Self { self.has_wifi = false; self } pub fn operating_system(mut self, os: impl Into<String>) -> Self { self.operating_system = os.into(); self } } // Only allow building when all required fields are set impl TypedComputerBuilder<CpuSet, RamSet> { pub fn build(self) -> Computer { Computer { cpu: self.cpu.unwrap(), ram: self.ram.unwrap(), storage: self.storage, has_graphics_card: self.has_graphics_card, has_wifi: self.has_wifi, operating_system: self.operating_system, } } } fn main() { // Simple builder usage let basic_computer = Computer::builder("Intel i5", "8GB") .build(); let gaming_computer = Computer::builder("Intel i9", "32GB") .storage("1TB NVMe SSD") .with_graphics_card() .operating_system("Windows 11 Pro") .build(); let server_computer = Computer::builder("AMD Threadripper", "128GB") .storage("2TB SSD RAID") .without_wifi() .operating_system("Ubuntu Server") .build(); println!("{:?}", basic_computer); println!("{:?}", gaming_computer); println!("{:?}", server_computer); // Type-state builder usage (compile-time safety) let typed_computer = TypedComputerBuilder::new() .cpu("Intel i7") .ram("16GB") .storage("512GB SSD") .with_graphics_card() .build(); println!("{:?}", typed_computer); // This would cause a compile error: // let incomplete = TypedComputerBuilder::new() // .cpu("Intel i7") // .build(); // Error: ram not set! }
Key Differences Between Java and Rust Implementations
1. Ownership and Move Semantics
- Java: Uses
return thisto enable method chaining, keeping the same object reference - Rust: Methods take
selfby value and returnSelf, using move semantics for the fluent interface
2. Memory Management
- Java: Relies on garbage collection; the builder object persists until GC
- Rust: The builder is consumed when
build()is called, preventing use-after-build errors at compile time
3. Type Safety
- Java: Runtime validation if required fields aren't set
- Rust: Can use the type-state pattern to enforce required fields at compile time
4. String Handling
- Java: Uses
Stringdirectly with implicit conversions - Rust: Uses
impl Into<String>for flexible string input (&str,String, etc.)
5. Null Safety
- Java: Potential for null pointer exceptions
- Rust: No null values; uses
Option<T>when needed
When to Use the Builder Pattern
The Builder pattern is ideal when:
- Objects have many optional parameters
- Object construction is complex or requires validation
- You want to enforce immutability in the final object
- The construction process should be independent of the object's representation
The Rust implementation particularly shines with its compile-time safety guarantees and zero-cost abstractions, making it both safer and potentially more performant than traditional OOP implementations.
Core Rust concepts introduced
Building on the concepts from the NewType pattern, the Builder pattern introduces several new Rust concepts:
1. Move Semantics in Method Chaining
#![allow(unused)] fn main() { pub fn storage(mut self, storage: impl Into<String>) -> Self { self.storage = storage.into(); self } }
- Taking
selfby value: Each builder method consumes the builder and returns a new one - Move semantics: The builder is moved through the chain, preventing accidental reuse
- Different from references: Unlike Java's
return this, Rust actually moves the data mut self: Explicitly declares that the method can modify the moved value
2. Trait Bounds and Generic Programming
#![allow(unused)] fn main() { pub fn new(cpu: impl Into<String>, ram: impl Into<String>) -> Self }
impl Into<String>: A trait bound allowing any type convertible toString- Flexible input: Accepts
&str,String,Cow<str>, etc. without explicit conversion - Compile-time resolution: More efficient than Java's method overloading
- Generic constraints: Specify what capabilities a type parameter must have
3. Advanced: PhantomData and Type-State Pattern
#![allow(unused)] fn main() { pub struct TypedComputerBuilder<CpuState = NotSet, RamState = NotSet> { _cpu_state: std::marker::PhantomData<CpuState>, _ram_state: std::marker::PhantomData<RamState>, } }
- Generic type parameters: Track builder state at compile time
- Default type parameters:
= NotSetprovides defaults for generics - PhantomData: Zero-sized type that carries type information without runtime cost
- Compile-time state tracking: Prevents calling
build()before required fields are set
4. Type-Level Programming
#![allow(unused)] fn main() { impl TypedComputerBuilder<NotSet, NotSet> { pub fn cpu(self) -> TypedComputerBuilder<CpuSet, NotSet> { ... } } impl TypedComputerBuilder<CpuSet, NotSet> { pub fn ram(self) -> TypedComputerBuilder<CpuSet, RamSet> { ... } } // Only this implementation has build() impl TypedComputerBuilder<CpuSet, RamSet> { pub fn build(self) -> Computer { ... } } }
- Different implementations for different type states: Compiler selects the right impl block
- Encoding logic in types: Use the type system to enforce correct usage patterns
- Compile-time method availability: Some methods only exist for certain type states
- State transitions: Methods transform types from one state to another
5. Compile-Time Guarantees vs Runtime Checks
#![allow(unused)] fn main() { pub fn build(self) -> Computer { // Returns Computer, not Option<Computer> Computer { cpu: self.cpu.unwrap(), // Safe because type system guarantees this exists // ... } } }
- Type-safe unwrapping:
unwrap()is safe when types guarantee the value exists - Eliminating runtime errors: Move error checking from runtime to compile time
- Performance benefit: No need for runtime validation when types encode correctness
6. Use-After-Move Prevention
#![allow(unused)] fn main() { let builder = ComputerBuilder::new("Intel i7", "16GB"); let computer = builder.build(); // builder is consumed here // builder.storage("1TB SSD"); // ❌ Compile error: builder was moved }
- Consuming methods: Methods that take
selfby value consume the object - Preventing reuse bugs: Can't accidentally use a builder after calling
build() - Memory safety: Eliminates entire classes of bugs at compile time
7. Zero-Cost State Machines
The type-state pattern demonstrates:
- Compile-time state machines: State transitions verified at compile time
- Zero runtime overhead: All type-state information is erased during compilation
- API safety: Impossible to call methods in wrong order or wrong state
- Documentation through types: The type system serves as living documentation
These advanced concepts showcase Rust's unique ability to encode complex invariants in the type system, providing both safety and performance benefits that are difficult to achieve in traditional object-oriented languages.
Iterator Pattern
The Iterator pattern provides a way to access elements of a collection sequentially without exposing the underlying representation of the collection. It's one of the most fundamental behavioral patterns and is deeply integrated into many modern programming languages.
Core Concept
The Iterator pattern defines a standard interface for traversing collections, allowing you to:
- Access elements one by one without knowing the internal structure
- Support multiple simultaneous traversals of the same collection
- Provide a uniform interface for different collection types
Java Implementation (Classical OOP)
In Java, the Iterator pattern is typically implemented using the Iterator interface:
import java.util.*;
// Custom collection implementing Iterable
class BookCollection implements Iterable<String> {
private List<String> books = new ArrayList<>();
public void addBook(String book) {
books.add(book);
}
@Override
public Iterator<String> iterator() {
return new BookIterator();
}
// Inner class implementing Iterator
private class BookIterator implements Iterator<String> {
private int currentIndex = 0;
@Override
public boolean hasNext() {
return currentIndex < books.size();
}
@Override
public String next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return books.get(currentIndex++);
}
}
}
// Usage example
public class IteratorExample {
public static void main(String[] args) {
BookCollection library = new BookCollection();
library.addBook("Design Patterns");
library.addBook("Clean Code");
library.addBook("Refactoring");
// Using iterator explicitly
Iterator<String> iter = library.iterator();
while (iter.hasNext()) {
System.out.println(iter.next());
}
// Using enhanced for-loop (syntactic sugar over iterator)
for (String book : library) {
System.out.println(book);
}
}
}
Rust Implementation
Rust takes a different approach with its iterator system, making it more functional and zero-cost through compile-time optimizations. Rust has two main iterator traits: Iterator and IntoIterator.
use std::vec::IntoIter; // Custom collection #[derive(Debug)] struct BookCollection { books: Vec<String>, } impl BookCollection { fn new() -> Self { Self { books: Vec::new() } } fn add_book(&mut self, book: String) { self.books.push(book); } } // Implementing IntoIterator for owned values impl IntoIterator for BookCollection { type Item = String; type IntoIter = IntoIter<String>; fn into_iter(self) -> Self::IntoIter { self.books.into_iter() } } // Implementing IntoIterator for references (borrowing) impl<'a> IntoIterator for &'a BookCollection { type Item = &'a String; type IntoIter = std::slice::Iter<'a, String>; fn into_iter(self) -> Self::IntoIter { self.books.iter() } } // Custom iterator implementation struct BookIterator<'a> { books: &'a [String], index: usize, } impl<'a> BookIterator<'a> { fn new(books: &'a [String]) -> Self { Self { books, index: 0 } } } impl<'a> Iterator for BookIterator<'a> { type Item = &'a String; fn next(&mut self) -> Option<Self::Item> { if self.index < self.books.len() { let book = &self.books[self.index]; self.index += 1; Some(book) } else { None } } } impl BookCollection { // Method returning custom iterator fn iter(&self) -> BookIterator<'_> { BookIterator::new(&self.books) } } fn main() { let mut library = BookCollection::new(); library.add_book("Design Patterns".to_string()); library.add_book("Clean Code".to_string()); library.add_book("Refactoring".to_string()); // Using custom iterator println!("Using custom iterator:"); for book in library.iter() { println!("{}", book); } // Using IntoIterator for references println!("\nUsing IntoIterator for references:"); for book in &library { println!("{}", book); } // Using iterator combinators (functional style) println!("\nUsing iterator combinators:"); let uppercase_books: Vec<String> = library.iter() .map(|book| book.to_uppercase()) .collect(); for book in &uppercase_books { println!("{}", book); } // Consuming the collection with IntoIterator println!("\nConsuming iteration:"); for book in library { // This moves library println!("Owned: {}", book); } // library is no longer accessible here due to move } // Advanced example: Lazy iterator with state struct FibonacciIterator { current: u64, next: u64, } impl FibonacciIterator { fn new() -> Self { Self { current: 0, next: 1 } } } impl Iterator for FibonacciIterator { type Item = u64; fn next(&mut self) -> Option<Self::Item> { let current = self.current; self.current = self.next; self.next = current + self.next; // Prevent overflow by stopping at large numbers if current > 1_000_000 { None } else { Some(current) } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_fibonacci_iterator() { let fib: Vec<u64> = FibonacciIterator::new() .take(10) .collect(); assert_eq!(fib, vec![0, 1, 1, 2, 3, 5, 8, 13, 21, 34]); } #[test] fn test_book_collection_iterator() { let mut library = BookCollection::new(); library.add_book("Book 1".to_string()); library.add_book("Book 2".to_string()); let books: Vec<&String> = library.iter().collect(); assert_eq!(books.len(), 2); } }
Key Differences Between Java and Rust
Java Approach:
- Object-oriented with explicit iterator objects
- Manual memory management through garbage collection
- Exception-based error handling (NoSuchElementException)
- Mutable iterator state through instance variables
- Enhanced for-loop as syntactic sugar
Rust Approach:
- Functional programming style with zero-cost abstractions
- Ownership system eliminates need for garbage collection
- Option-based error handling (None instead of exceptions)
- Immutable by default with explicit mutability
- Rich set of iterator combinators (map, filter, fold, etc.)
- Lazy evaluation - iterators do nothing until consumed
Benefits of Rust's Iterator Design
- Zero-cost abstractions: Rust's iterators compile down to the same code as hand-written loops
- Memory safety: The ownership system prevents iterator invalidation bugs
- Functional composition: Chain operations together naturally
- Lazy evaluation: Only compute values when needed
- Explicit ownership: Clear distinction between borrowing and consuming iteration
The Iterator pattern in Rust demonstrates how the language takes classical design patterns and reimagines them through the lens of ownership, safety, and zero-cost abstractions, often resulting in more expressive and efficient code than traditional OOP implementations.
Core Rust concepts introduced
Building on the NewType and Builder patterns, the Iterator pattern introduces several new concepts:
1. Lifetimes and Lifetime Parameters
#![allow(unused)] fn main() { impl<'a> IntoIterator for &'a BookCollection { type Item = &'a String; type IntoIter = std::slice::Iter<'a, String>; } struct BookIterator<'a> { books: &'a [String], index: usize, } }
- Lifetime parameters (
'a): Tell the compiler how long references are valid - Lifetime annotations: Connect the lifetime of input and output references
- Borrow checker validation: Ensures references don't outlive the data they point to
- Generic over lifetimes: Structs and functions can be parameterized by lifetimes
2. Mutable References and Borrowing Rules
#![allow(unused)] fn main() { fn next(&mut self) -> Option<Self::Item> { self.index += 1; // Can modify because of &mut self // ... } fn iter(&self) -> BookIterator { // Cannot modify self, only read BookIterator::new(&self.books) } }
&mut self: Exclusive mutable access to modify the iterator state- Borrowing rules: Only one mutable reference OR multiple immutable references
- Interior mutability: Some types allow mutation through shared references (not shown here)
3. Closure Syntax and Capture
#![allow(unused)] fn main() { .map(|book| book.to_uppercase()) .filter(|&book| book.len() > 5) }
- Closure syntax:
|param| expressionfor anonymous functions - Capture by reference: Closures can borrow values from their environment
- Capture by value: Use
movekeyword to take ownership of captured variables - Closure traits:
Fn,FnMut, andFnOncedefine how closures interact with captured data
4. Iterator Combinators and Lazy Evaluation
#![allow(unused)] fn main() { let uppercase_books: Vec<String> = library.iter() .map(|book| book.to_uppercase()) // Transform each element .filter(|book| book.contains("RUST")) // Keep only matching elements .collect(); // Consume iterator into collection }
- Lazy evaluation: Iterators do nothing until consumed by methods like
collect() - Method chaining: Combine multiple operations in a fluent interface
- Adapters vs consumers:
map/filterare adapters,collect/foldare consumers - Zero-cost: The entire chain compiles to an efficient loop
5. Type Inference and the Turbofish
#![allow(unused)] fn main() { let books: Vec<&String> = library.iter().collect(); // Or with explicit type specification: let books = library.iter().collect::<Vec<&String>>(); }
- Type inference: Rust deduces types from context when possible
- Turbofish syntax (
::<Type>): Explicitly specify generic type parameters - When inference fails: Need explicit types when compiler can't determine the target type
6. Slice Types and Fat Pointers
#![allow(unused)] fn main() { struct BookIterator<'a> { books: &'a [String], // Slice reference index: usize, } }
- Slice types (
&[T]): References to a contiguous sequence of elements - Fat pointers: Slices contain both a pointer and a length
- No bounds checking overhead: Slice length is known at runtime
- Memory efficient: Just a reference to existing data, no allocation
7. Consuming vs Borrowing Iteration
#![allow(unused)] fn main() { // Borrowing iteration - collection remains usable for book in &library { println!("{}", book); } // Consuming iteration - collection is moved for book in library { // library is no longer accessible after this println!("Owned: {}", book); } }
- Different iteration modes:
&collectionvscollectionvs&mut collection - Move semantics in loops:
for book in librarymoves the entire collection - Preventing use-after-move: Compiler error if you try to use
libraryafter consuming iteration
8. Associated Types in Traits
#![allow(unused)] fn main() { impl Iterator for BookIterator<'a> { type Item = &'a String; // Associated type fn next(&mut self) -> Option<Self::Item> { // ... } } }
- Associated types: Types that are "associated" with a trait implementation
Self::Item: Refers to the associated type within the implementation- Cleaner than generics: Often more readable than generic type parameters
9. Infinite Iterators and State
#![allow(unused)] fn main() { struct FibonacciIterator { current: u64, next: u64, } impl Iterator for FibonacciIterator { type Item = u64; fn next(&mut self) -> Option<Self::Item> { let current = self.current; self.current = self.next; self.next = current + self.next; if current > 1_000_000 { None // Stop at large numbers } else { Some(current) } } } }
- Stateful iterators: Iterators can maintain internal state between calls
- Infinite sequences: Iterators don't need to have a predetermined end
- Conditional termination: Return
Noneto signal the end of iteration
These concepts demonstrate Rust's approach to safe, efficient iteration that eliminates common bugs like iterator invalidation while providing zero-cost abstractions and powerful functional programming capabilities.
Result/Error Handling Pattern
The Result/Error Handling pattern in Rust is a fundamental approach to error management that makes error handling explicit and type-safe. While not strictly one of the Gang of Four design patterns, it's a crucial pattern in Rust that provides a robust alternative to exception-based error handling found in classical OOP languages like Java.
What is the Result Pattern?
The Result pattern uses Rust's Result<T, E> enum to represent either a successful value (Ok(T)) or an error (Err(E)). This forces developers to explicitly handle both success and failure cases, preventing runtime crashes from unhandled exceptions.
Comparison with Java
In Java, error handling typically uses exceptions:
// Java - Exception-based error handling
public class FileProcessor {
public String readFile(String filename) throws IOException {
// May throw IOException
return Files.readString(Paths.get(filename));
}
public void processFile(String filename) {
try {
String content = readFile(filename);
System.out.println("Content: " + content);
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
}
}
}
Rust Implementation
use std::fs; use std::io; use std::fmt; // Custom error type for our application #[derive(Debug)] enum FileProcessError { IoError(io::Error), InvalidContent(String), EmptyFile, } impl fmt::Display for FileProcessError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { FileProcessError::IoError(err) => write!(f, "IO error: {}", err), FileProcessError::InvalidContent(msg) => write!(f, "Invalid content: {}", msg), FileProcessError::EmptyFile => write!(f, "File is empty"), } } } impl From<io::Error> for FileProcessError { fn from(error: io::Error) -> Self { FileProcessError::IoError(error) } } struct FileProcessor; impl FileProcessor { // Returns Result<T, E> instead of throwing exceptions fn read_file(filename: &str) -> Result<String, FileProcessError> { let content = fs::read_to_string(filename)?; // ? operator for error propagation if content.is_empty() { return Err(FileProcessError::EmptyFile); } Ok(content) } // Validates file content fn validate_content(content: &str) -> Result<&str, FileProcessError> { if content.len() < 10 { return Err(FileProcessError::InvalidContent( "Content too short".to_string() )); } Ok(content) } // Chains multiple Result operations fn process_file(filename: &str) -> Result<String, FileProcessError> { let content = Self::read_file(filename)?; let validated_content = Self::validate_content(&content)?; // Process the content (just uppercase for this example) Ok(validated_content.to_uppercase()) } } fn main() { let filenames = vec!["example.txt", "nonexistent.txt", "empty.txt"]; for filename in filenames { // Pattern matching for error handling match FileProcessor::process_file(filename) { Ok(processed_content) => { println!("✅ Successfully processed {}: {}", filename, &processed_content[..50.min(processed_content.len())]); } Err(error) => { println!("❌ Error processing {}: {}", filename, error); } } } // Alternative error handling approaches demonstrate_error_handling_patterns(); } fn demonstrate_error_handling_patterns() { println!("\n--- Different Error Handling Patterns ---"); // 1. Using unwrap_or for default values let result1 = FileProcessor::read_file("nonexistent.txt") .unwrap_or_else(|_| "Default content".to_string()); println!("With default: {}", result1); // 2. Using map and map_err for transformations let result2 = FileProcessor::read_file("example.txt") .map(|content| content.len()) .map_err(|err| format!("Transformed error: {}", err)); match result2 { Ok(length) => println!("Content length: {}", length), Err(err) => println!("Error: {}", err), } // 3. Chaining operations with and_then let result3 = FileProcessor::read_file("example.txt") .and_then(|content| { FileProcessor::validate_content(&content)?; Ok(content) }) .map(|content| format!("Processed: {}", content.to_uppercase())); match result3 { Ok(processed) => println!("{}", processed), Err(err) => println!("Chain error: {}", err), } } // Demonstrating the ? operator sugar fn complex_operation(filename: &str) -> Result<usize, FileProcessError> { // Each ? will return early if there's an error let content = FileProcessor::read_file(filename)?; let validated = FileProcessor::validate_content(&content)?; let processed = validated.to_uppercase(); Ok(processed.len()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_successful_processing() { // Create a temporary file for testing std::fs::write("test_file.txt", "This is a test file with enough content").unwrap(); let result = FileProcessor::process_file("test_file.txt"); assert!(result.is_ok()); // Cleanup std::fs::remove_file("test_file.txt").ok(); } #[test] fn test_empty_file_error() { // Create an empty file std::fs::write("empty_test.txt", "").unwrap(); let result = FileProcessor::read_file("empty_test.txt"); assert!(matches!(result, Err(FileProcessError::EmptyFile))); // Cleanup std::fs::remove_file("empty_test.txt").ok(); } }
Key Advantages of Rust's Result Pattern
**1. Explicit Error Handling: Unlike Java's checked exceptions that can be ignored or forgotten, Rust's Result type forces you to handle errors explicitly.
**2. No Hidden Control Flow: Unlike exceptions that can unwind the stack unpredictably, Results make error propagation explicit and traceable.
**3. Composability: Results can be chained using methods like map, and_then, unwrap_or, making error handling both functional and readable.
**4. Zero-Cost Abstractions: The ? operator provides clean syntax without runtime overhead.
**5. Type Safety: The compiler ensures you handle all possible error cases, preventing runtime crashes from unhandled exceptions.
Comparison Summary
| Aspect | Java (Exceptions) | Rust (Result) |
|---|---|---|
| Error Visibility | Hidden until runtime | Explicit in type system |
| Handling Requirement | Optional (can ignore) | Mandatory |
| Performance | Stack unwinding overhead | Zero-cost |
| Composability | Limited | Highly composable |
| Control Flow | Non-local (jumps) | Local and explicit |
The Result pattern exemplifies Rust's philosophy of "making errors impossible to ignore" while providing ergonomic tools for error handling that are both safer and more performant than traditional exception-based approaches.
Core Rust concepts introduced
Building on the previous patterns, the Error Handling pattern introduces several new concepts:
1. Enums with Data and Pattern Matching
#![allow(unused)] fn main() { #[derive(Debug)] enum FileProcessError { IoError(io::Error), // Variant holding another error type InvalidContent(String), // Variant holding a String EmptyFile, // Unit variant (no data) } match FileProcessor::process_file(filename) { Ok(processed_content) => { /* handle success */ } Err(error) => { /* handle error */ } } }
- Enum variants with data: Each variant can hold different types of data
- Exhaustive pattern matching:
matchrequires handling all possible cases - Destructuring in patterns: Extract data from enum variants
- Sum types: Enums represent "one of" several possible values
2. The ? Operator and Error Propagation
#![allow(unused)] fn main() { let content = fs::read_to_string(filename)?; // Early return on error let validated = Self::validate_content(&content)?; }
- Early return shorthand:
?returns immediately ifResultisErr - Automatic conversion: Uses
Fromtrait to convert between error types - Syntactic sugar: Eliminates verbose
matchstatements for error propagation - Function signature requirement: Function must return
Resultto use?
3. From Trait and Automatic Error Conversion
#![allow(unused)] fn main() { impl From<io::Error> for FileProcessError { fn from(error: io::Error) -> Self { FileProcessError::IoError(error) } } }
- Automatic conversions:
?operator usesFromimplementations - Error type unification: Convert different error types to a common error type
- Composable error handling: Build error hierarchies without manual conversion
- Zero-cost conversion:
Fromimplementations are inlined
4. Result Combinators and Functional Error Handling
#![allow(unused)] fn main() { let result = FileProcessor::read_file("file.txt") .map(|content| content.len()) // Transform success value .map_err(|err| format!("Error: {}", err)) // Transform error value .and_then(|len| if len > 0 { Ok(len) } else { Err("Empty".to_string()) }); }
- Functional composition: Chain operations that might fail
map: Transform the success value, leave errors unchangedmap_err: Transform the error value, leave success unchangedand_then: Chain operations that also returnResult- Monadic interface: Results form a monad for error handling
5. Error Display and Debug Traits
#![allow(unused)] fn main() { impl fmt::Display for FileProcessError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { FileProcessError::IoError(err) => write!(f, "IO error: {}", err), FileProcessError::InvalidContent(msg) => write!(f, "Invalid content: {}", msg), FileProcessError::EmptyFile => write!(f, "File is empty"), } } } }
- Display trait: Defines user-facing error messages
- Debug trait: Defines debugging representation (via
#[derive(Debug)]) - Error trait: Standard trait for error types (not shown but commonly used)
- Hierarchical error information: Errors can wrap and display other errors
6. Unit Structs for Namespacing
#![allow(unused)] fn main() { struct FileProcessor; // Unit struct with no fields impl FileProcessor { fn read_file(filename: &str) -> Result<String, FileProcessError> { /* ... */ } } }
- Unit structs: Types with no data, used for namespacing
- Zero-size types: No runtime memory cost
- Associated functions: Group related functions under a type
- No inheritance: Use modules and types for organization instead
7. Advanced Error Handling Patterns
#![allow(unused)] fn main() { // Chaining with and_then for complex validation let result = FileProcessor::read_file("file.txt") .and_then(|content| { FileProcessor::validate_content(&content)?; Ok(content) }) .map(|content| content.to_uppercase()); }
- Error short-circuiting: First error stops the entire chain
- Nested error handling: Use
?inside closures passed toand_then - Transformation pipelines: Build complex processing pipelines
- Fail-fast behavior: Errors propagate immediately without executing later steps
8. Testing Error Conditions
#![allow(unused)] fn main() { #[test] fn test_empty_file_error() { let result = FileProcessor::read_file("empty_test.txt"); assert!(matches!(result, Err(FileProcessError::EmptyFile))); } }
matches!macro: Pattern matching in assertions- Testing error paths: Verify that errors occur under expected conditions
- Specific error checking: Test for exact error variants, not just failure
9. Errors as Values Philosophy
This pattern demonstrates Rust's fundamental approach to error handling:
- No hidden control flow: Errors don't "jump" through the call stack
- Explicit in types: Function signatures show whether they can fail
- Composable: Errors can be transformed, chained, and combined
- Performance: No stack unwinding or exception handling overhead
These concepts show how Rust's type system makes error handling both safer and more explicit than exception-based systems, while providing powerful tools for composing error-handling logic.
RAII/Dropdown Pattern
The RAII (Resource Acquisition Is Initialization) pattern, combined with Rust's Drop trait, is a fundamental resource management pattern that ensures resources are automatically cleaned up when they go out of scope. This isn't one of the original Gang of Four patterns, but it's so central to Rust's design philosophy that it's worth understanding in the context of design patterns.
What is RAII/Drop?
RAII is a programming idiom where resource acquisition (memory allocation, file handles, network connections, etc.) is tied to object initialization, and resource deallocation is tied to object destruction. In Rust, this is implemented through the Drop trait, which provides a drop method that's automatically called when a value goes out of scope.
Comparison with Java
In Java, you typically manage resources using try-with-resources or manual cleanup:
// Java - Manual resource management
public class FileManager {
private FileInputStream file;
public FileManager(String filename) throws IOException {
this.file = new FileInputStream(filename);
}
public void close() throws IOException {
if (file != null) {
file.close();
}
}
// Or using try-with-resources
public static void processFile(String filename) {
try (FileInputStream file = new FileInputStream(filename)) {
// Use file
} catch (IOException e) {
// Handle error
}
}
}
Rust Implementation
use std::fs::File; use std::io::{BufRead, BufReader, Read}; // Example 1: Custom resource with Drop implementation struct DatabaseConnection { connection_id: u32, is_connected: bool, } impl DatabaseConnection { fn new(id: u32) -> Self { println!("Opening database connection {}", id); DatabaseConnection { connection_id: id, is_connected: true, } } fn execute_query(&self, query: &str) -> std::result::Result<Vec<String>, Box<dyn std::error::Error>> { if !self.is_connected { return Err("Connection is closed".into()); } println!("Executing query on connection {}: {}", self.connection_id, query); Ok(vec!["result1".to_string(), "result2".to_string()]) } } // Implement Drop trait for automatic cleanup impl Drop for DatabaseConnection { fn drop(&mut self) { if self.is_connected { println!("Closing database connection {}", self.connection_id); self.is_connected = false; } } } // Example 2: File manager using RAII struct FileManager { file: BufReader<File>, filename: String, } impl FileManager { fn new(filename: &str) -> std::io::Result<Self> { println!("Opening file: {}", filename); let file = File::open(filename)?; let reader = BufReader::new(file); Ok(FileManager { file: reader, filename: filename.to_string(), }) } fn read_lines(&mut self) -> std::io::Result<Vec<String>> { let mut lines = Vec::new(); for line in self.file.by_ref().lines() { lines.push(line?); } Ok(lines) } } impl Drop for FileManager { fn drop(&mut self) { println!("Closing file: {}", self.filename); // File is automatically closed when BufReader is dropped } } // Example 3: Smart pointer-like wrapper struct SmartResource<T> { resource: Option<T>, name: String, } impl<T> SmartResource<T> { fn new(resource: T, name: String) -> Self { println!("Acquiring resource: {}", name); SmartResource { resource: Some(resource), name, } } fn get(&self) -> Option<&T> { self.resource.as_ref() } fn take(mut self) -> Option<T> { self.resource.take() } } impl<T> Drop for SmartResource<T> { fn drop(&mut self) { if self.resource.is_some() { println!("Releasing resource: {}", self.name); } } } // Example 4: Scoped guard pattern struct ScopeGuard<F: FnOnce()> { cleanup: Option<F>, } impl<F: FnOnce()> ScopeGuard<F> { fn new(cleanup: F) -> Self { ScopeGuard { cleanup: Some(cleanup), } } fn disarm(mut self) { self.cleanup = None; } } impl<F: FnOnce()> Drop for ScopeGuard<F> { fn drop(&mut self) { if let Some(cleanup) = self.cleanup.take() { cleanup(); } } } fn main() -> std::result::Result<(), Box<dyn std::error::Error>> { println!("=== RAII/Drop Pattern Examples ===\n"); // Example 1: Database connection { println!("1. Database Connection Example:"); let db = DatabaseConnection::new(1); let _results = db.execute_query("SELECT * FROM users")?; // db is automatically dropped here, connection closed println!(); } // Example 2: File manager { println!("2. File Manager Example:"); // Create a temporary file for demonstration std::fs::write("temp.txt", "Hello\nWorld\nRust\nRAII")?; { let mut file_mgr = FileManager::new("temp.txt")?; let lines = file_mgr.read_lines()?; println!("Read {} lines", lines.len()); // file_mgr is dropped here, file closed automatically } // Clean up std::fs::remove_file("temp.txt").ok(); println!(); } // Example 3: Smart resource { println!("3. Smart Resource Example:"); let data = vec![1, 2, 3, 4, 5]; let smart_res = SmartResource::new(data, "Vector Data".to_string()); if let Some(vec_ref) = smart_res.get() { println!("Vector length: {}", vec_ref.len()); } // smart_res is dropped here println!(); } // Example 4: Scope guard { println!("4. Scope Guard Example:"); let _guard = ScopeGuard::new(|| { println!("Cleanup code executed!"); }); println!("Doing some work..."); // guard's cleanup function will be called when _guard is dropped println!(); } // Example 5: Early cleanup { println!("5. Early Cleanup Example:"); let mut counter = 0; let guard = ScopeGuard::new(|| { println!("This cleanup will be skipped"); }); counter += 1; if counter > 0 { guard.disarm(); // Prevent cleanup println!("Guard disarmed, cleanup won't run"); } println!(); } println!("=== All resources cleaned up automatically ==="); Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_database_connection_cleanup() { // This test demonstrates that Drop is called even if we don't explicitly close let db = DatabaseConnection::new(999); assert!(db.is_connected); // db will be dropped at end of scope, calling Drop::drop } #[test] fn test_smart_resource_take() { let data = "test data".to_string(); let mut smart_res = SmartResource::new(data, "Test Resource".to_string()); let taken = smart_res.take(); assert!(taken.is_some()); assert_eq!(taken.unwrap(), "test data"); // Resource was taken, so Drop won't print release message } #[test] fn test_scope_guard_disarm() { let mut cleanup_called = false; { let guard = ScopeGuard::new(|| { // This won't be called due to disarm }); guard.disarm(); } // Cleanup should not have been called } }
Key Benefits of RAII/Drop in Rust
-
Automatic Resource Management: Resources are automatically cleaned up when they go out of scope, eliminating memory leaks and resource leaks.
-
Exception Safety: Even if a panic occurs, Drop is still called during stack unwinding (unless the panic occurs during unwinding itself).
-
Zero-Cost Abstraction: The Drop trait adds no runtime overhead - cleanup code is inserted at compile time.
-
Composability: Types that contain other types with Drop implementations automatically get proper cleanup behavior.
Comparison with Gang of Four Patterns
While RAII/Drop isn't a GoF pattern, it relates to several:
- Proxy Pattern: Smart pointers like
Box<T>,Rc<T>, andArc<T>use RAII - Decorator Pattern: Wrappers that add behavior while maintaining automatic cleanup
- Template Method Pattern: The Drop trait defines the "template" for cleanup behavior
Best Practices
- Always implement Drop for resource-owning types
- Use
Option<T>to allow early resource release - Consider providing explicit cleanup methods for user control
- Use scope guards for complex cleanup scenarios
- Be careful with cyclic references in
Rc<T>- they won't be cleaned up automatically
The RAII/Drop pattern is fundamental to Rust's memory safety guarantees and makes resource management much more reliable compared to manual cleanup approaches common in other languages.
Core Rust concepts introduced
Building on all previous patterns, the RAII/Drop pattern introduces the final set of core concepts:
1. The Drop Trait and Automatic Cleanup
#![allow(unused)] fn main() { impl Drop for DatabaseConnection { fn drop(&mut self) { if self.is_connected { println!("Closing database connection {}", self.connection_id); self.is_connected = false; } } } }
- Drop trait: Defines custom cleanup behavior when values go out of scope
- Automatic invocation:
drop()is called automatically, no manual cleanup needed - Deterministic destruction: Cleanup happens at predictable, compile-time-known points
- Exception safety: Drop runs even during panic unwinding (in most cases)
2. Scope-Based Resource Management (RAII)
#![allow(unused)] fn main() { { let db = DatabaseConnection::new(1); // use db... } // db.drop() is automatically called here }
- Lexical scoping: Resource lifetime tied to variable scope
- Stack unwinding: Resources cleaned up in reverse order of creation
- No manual cleanup: Compiler automatically inserts cleanup code
- Resource acquisition is initialization: Resources acquired in constructors, released in destructors
3. Consuming Methods and Resource Transfer
#![allow(unused)] fn main() { fn take(mut self) -> Option<T> { // Takes ownership of self self.resource.take() // Removes value from Option } }
- Consuming methods: Methods that take
selfby value - Resource transfer: Move resources out of containers
- Preventing double-cleanup: Taking a resource prevents Drop from running on it
- Option::take(): Standard way to move values out of Option
4. Advanced Closure Patterns
#![allow(unused)] fn main() { struct ScopeGuard<F: FnOnce()> { cleanup: Option<F>, } let _guard = ScopeGuard::new(|| { println!("Cleanup code executed!"); }); }
FnOnce()trait bound: Closure that can be called exactly once- Closure capture: Functions can capture variables from their environment
- Scope guards: Pattern for running code when leaving a scope
- Generic over functions: Types can be parameterized by function types
5. Pattern Matching with Option::take()
#![allow(unused)] fn main() { impl<F: FnOnce()> Drop for ScopeGuard<F> { fn drop(&mut self) { if let Some(cleanup) = self.cleanup.take() { cleanup(); } } } }
- Option::take(): Moves value out of Option, leaving None
- Conditional execution: Only run cleanup if it hasn't been "disarmed"
- Preventing double-execution: Taking the closure prevents calling it twice
- if let pattern: Concise way to handle the Some case
6. Disarming Pattern
#![allow(unused)] fn main() { fn disarm(mut self) { self.cleanup = None; } }
- Manual resource control: Sometimes you want to prevent automatic cleanup
- Disarming guards: Remove the cleanup function to prevent execution
- Consuming self: Method takes ownership to prevent further use
- Explicit control: Provides escape hatch from automatic behavior
7. Unit Types and Zero-Sized Types
#![allow(unused)] fn main() { struct FileProcessor; // Zero-sized type }
- Unit types: Types with no data, used for namespacing or marker types
- Zero runtime cost: These types are completely optimized away
- Namespacing: Group related functions without creating instances
- Marker types: Used in type-level programming (phantom types)
8. Generic Type Parameters with Trait Bounds
#![allow(unused)] fn main() { struct SmartResource<T> { resource: Option<T>, name: String, } impl<T> SmartResource<T> { // Implementation for any type T fn new(resource: T, name: String) -> Self { ... } } }
- Generic structs: Types parameterized by other types
- Impl blocks: Implementations work for any type T
- Monomorphization: Compiler generates specialized code for each concrete type
- Zero-cost generics: No runtime overhead for generic abstraction
9. Resource Management Patterns
The RAII pattern demonstrates several important resource management concepts:
- Automatic cleanup: No need to remember to call cleanup functions
- Exception safety: Cleanup happens even if code panics
- Composability: Types containing RAII types automatically get proper cleanup
- Performance: No garbage collection overhead, deterministic timing
10. Testing Automatic Behavior
#![allow(unused)] fn main() { #[test] fn test_database_connection_cleanup() { let db = DatabaseConnection::new(999); assert!(db.is_connected); // db will be dropped at end of scope, calling Drop::drop } }
- Testing destructors: Verify that cleanup code runs correctly
- Scope-based testing: Let variables go out of scope to trigger Drop
- Side effect verification: Check that cleanup side effects occurred
These final concepts complete the foundation of Rust's memory management model. The Drop trait and RAII pattern showcase how Rust achieves memory safety and resource safety without garbage collection, making resource management both automatic and predictable.
Chapter 2: Ownership & Borrowing Mastery
This chapter delves deeper into Rust's ownership model and borrowing system through design patterns that showcase advanced memory management techniques. Building on the foundation patterns from Chapter 1, these patterns will teach you to leverage Rust's unique features for efficient, safe, and elegant code. After mastering these patterns, you'll have a deep understanding of how to work with Rust's ownership system rather than fighting against it.
Patterns Covered
Strategy Pattern
Simple trait usage, dynamic dispatch vs static dispatch
Core Rust concepts introduced:
- Trait Objects and Dynamic Dispatch (
dyn Trait) - Static vs Dynamic Dispatch Trade-offs
- Object Safety Rules
- Vtables and Runtime Polymorphism
- Box Smart Pointers for Heap Allocation
- Performance Implications of Dispatch Types
Adapter Pattern
Trait implementations for foreign types, orphan rule
New Rust concepts introduced:
- The Orphan Rule and Coherence
- Newtype Wrapper for External Types
- Foreign Function Interface (FFI) Considerations
- Trait Implementation Restrictions
- Upstream/Downstream Crate Relationships
- Blanket Implementations and Conflicts
Interior Mutability Pattern
When and why to use Cell/RefCell/Mutex
New Rust concepts introduced:
- Interior Mutability vs Inherited Mutability
Cell<T>for Copy TypesRefCell<T>for Runtime Borrow CheckingMutex<T>for Thread-Safe Interior Mutability- Runtime vs Compile-Time Borrow Checking
- Shared Ownership with
Rc<RefCell<T>> - Memory Safety Trade-offs
Cow (Clone on Write)
Efficient memory usage, understanding when cloning is needed
New Rust concepts introduced:
Cow<'a, T>Enum and Borrowed/Owned Variants- Lazy Cloning and Performance Optimization
- Lifetime Elision in Return Types
ToOwnedTrait and Custom Implementations- Zero-Copy String Processing
- API Design for Flexible Input Types
- Memory Efficiency Patterns
Command Pattern
Closures, function pointers, and ownership of captured variables
New Rust concepts introduced:
- Closure Traits:
Fn,FnMut, andFnOnce - Function Pointers (
fn) vs Closures - Capture Modes: By Reference, By Value, By Move
moveKeyword for Closure Ownership- Boxed Closures for Dynamic Storage
- Higher-Order Functions and Combinators
- Callback Patterns and Event Systems
Learning Approach
Each pattern in this chapter builds upon the ownership concepts from Chapter 1 while introducing more sophisticated borrowing and memory management techniques. You'll learn:
- When to use different dispatch mechanisms and their performance implications
- How to work with external types while respecting Rust's coherence rules
- When compile-time borrowing isn't enough and how to safely use runtime alternatives
- How to avoid unnecessary cloning while maintaining API flexibility
- How to capture and store computation using Rust's powerful closure system
Pattern Structure
Each pattern chapter follows this enhanced structure:
- Problem Statement - Real-world scenarios where the pattern applies
- Traditional Implementation - How other languages approach the problem
- Rust Implementation - Leveraging ownership and borrowing for better solutions
- Performance Analysis - Memory and runtime characteristics
- Best Practices - When to use each approach and common pitfalls
- Advanced Techniques - Pushing the pattern to its limits
- Core Rust Concepts Introduced - New ownership and borrowing features
By the end of this chapter, you'll understand Rust's ownership model at a deep level and be able to choose the right ownership patterns for complex scenarios. You'll also be prepared for the concurrent and parallel patterns in later chapters.
Strategy Pattern
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime. In Rust, this pattern showcases the power of traits for both static and dynamic dispatch, allowing you to choose between compile-time optimization and runtime flexibility.
Problem Statement
You need to switch between different algorithms or behaviors at runtime based on configuration, user input, or changing conditions. Traditional object-oriented languages use inheritance or interfaces, but Rust's trait system provides more powerful and efficient alternatives.
Traditional Object-Oriented Implementation
In Java, the Strategy pattern typically uses interfaces and classes:
// Strategy interface
interface PaymentStrategy {
void pay(double amount);
}
// Concrete strategies
class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " using credit card: " + cardNumber);
}
}
class PayPalPayment implements PaymentStrategy {
private String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " using PayPal: " + email);
}
}
// Context class
class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(double amount) {
paymentStrategy.pay(amount);
}
}
Rust Implementation
Rust provides multiple ways to implement the Strategy pattern, each with different trade-offs:
1. Static Dispatch with Generics
#![allow(unused)] fn main() { // Strategy trait trait PaymentStrategy { fn pay(&self, amount: f64); } // Concrete strategies struct CreditCardPayment { card_number: String, } impl CreditCardPayment { fn new(card_number: String) -> Self { Self { card_number } } } impl PaymentStrategy for CreditCardPayment { fn pay(&self, amount: f64) { println!("Paid ${:.2} using credit card: {}", amount, self.card_number); } } struct PayPalPayment { email: String, } impl PayPalPayment { fn new(email: String) -> Self { Self { email } } } impl PaymentStrategy for PayPalPayment { fn pay(&self, amount: f64) { println!("Paid ${:.2} using PayPal: {}", amount, self.email); } } // Context with static dispatch struct ShoppingCart<T: PaymentStrategy> { payment_strategy: T, items: Vec<(String, f64)>, } impl<T: PaymentStrategy> ShoppingCart<T> { fn new(payment_strategy: T) -> Self { Self { payment_strategy, items: Vec::new(), } } fn add_item(&mut self, name: String, price: f64) { self.items.push((name, price)); } fn checkout(&self) { let total: f64 = self.items.iter().map(|(_, price)| price).sum(); self.payment_strategy.pay(total); } } }
2. Dynamic Dispatch with Trait Objects
#![allow(unused)] fn main() { // Using Box<dyn Trait> for owned trait objects struct DynamicShoppingCart { payment_strategy: Box<dyn PaymentStrategy>, items: Vec<(String, f64)>, } impl DynamicShoppingCart { fn new(payment_strategy: Box<dyn PaymentStrategy>) -> Self { Self { payment_strategy, items: Vec::new(), } } fn set_payment_strategy(&mut self, strategy: Box<dyn PaymentStrategy>) { self.payment_strategy = strategy; } fn add_item(&mut self, name: String, price: f64) { self.items.push((name, price)); } fn checkout(&self) { let total: f64 = self.items.iter().map(|(_, price)| price).sum(); self.payment_strategy.pay(total); } } // Alternative: Using reference to trait object fn process_payment(strategy: &dyn PaymentStrategy, amount: f64) { strategy.pay(amount); } }
3. Enum-Based Strategy (Rust Idiomatic)
#![allow(unused)] fn main() { // Enum-based approach - often more idiomatic in Rust #[derive(Debug, Clone)] enum PaymentMethod { CreditCard { card_number: String }, PayPal { email: String }, BankTransfer { account_number: String }, Cryptocurrency { wallet_address: String }, } impl PaymentMethod { fn pay(&self, amount: f64) { match self { PaymentMethod::CreditCard { card_number } => { println!("Paid ${:.2} using credit card: {}", amount, card_number); } PaymentMethod::PayPal { email } => { println!("Paid ${:.2} using PayPal: {}", amount, email); } PaymentMethod::BankTransfer { account_number } => { println!("Paid ${:.2} using bank transfer: {}", amount, account_number); } PaymentMethod::Cryptocurrency { wallet_address } => { println!("Paid ${:.2} using crypto wallet: {}", amount, wallet_address); } } } } struct ModernShoppingCart { payment_method: PaymentMethod, items: Vec<(String, f64)>, } impl ModernShoppingCart { fn new(payment_method: PaymentMethod) -> Self { Self { payment_method, items: Vec::new(), } } fn set_payment_method(&mut self, method: PaymentMethod) { self.payment_method = method; } fn add_item(&mut self, name: String, price: f64) { self.items.push((name, price)); } fn checkout(&self) { let total: f64 = self.items.iter().map(|(_, price)| price).sum(); self.payment_method.pay(total); } } }
4. Advanced: Function Pointer Strategy
#![allow(unused)] fn main() { // Using function pointers for strategy type PaymentFunction = fn(f64, &str); fn credit_card_payment(amount: f64, details: &str) { println!("Paid ${:.2} using credit card: {}", amount, details); } fn paypal_payment(amount: f64, details: &str) { println!("Paid ${:.2} using PayPal: {}", amount, details); } struct FunctionShoppingCart { payment_fn: PaymentFunction, payment_details: String, items: Vec<(String, f64)>, } impl FunctionShoppingCart { fn new(payment_fn: PaymentFunction, payment_details: String) -> Self { Self { payment_fn, payment_details, items: Vec::new(), } } fn checkout(&self) { let total: f64 = self.items.iter().map(|(_, price)| price).sum(); (self.payment_fn)(total, &self.payment_details); } } }
Complete Example and Usage
fn main() { println!("=== Strategy Pattern Examples ===\n"); // 1. Static dispatch example println!("1. Static Dispatch:"); let mut static_cart = ShoppingCart::new(CreditCardPayment::new("1234-5678-9012-3456".to_string())); static_cart.add_item("Laptop".to_string(), 999.99); static_cart.add_item("Mouse".to_string(), 29.99); static_cart.checkout(); // 2. Dynamic dispatch example println!("\n2. Dynamic Dispatch:"); let mut dynamic_cart = DynamicShoppingCart::new( Box::new(PayPalPayment::new("user@example.com".to_string())) ); dynamic_cart.add_item("Keyboard".to_string(), 79.99); dynamic_cart.checkout(); // Switch strategy at runtime dynamic_cart.set_payment_strategy( Box::new(CreditCardPayment::new("9876-5432-1098-7654".to_string())) ); dynamic_cart.checkout(); // 3. Enum-based approach println!("\n3. Enum-based Strategy:"); let mut modern_cart = ModernShoppingCart::new( PaymentMethod::Cryptocurrency { wallet_address: "1A2B3C4D5E6F7G8H9I0J".to_string() } ); modern_cart.add_item("Graphics Card".to_string(), 599.99); modern_cart.checkout(); // 4. Function pointer approach println!("\n4. Function Pointer Strategy:"); let mut fn_cart = FunctionShoppingCart::new( credit_card_payment, "4444-3333-2222-1111".to_string() ); fn_cart.items.push(("Monitor".to_string(), 299.99)); fn_cart.checkout(); // 5. Demonstrating runtime strategy selection println!("\n5. Runtime Strategy Selection:"); demonstrate_runtime_selection(); } fn demonstrate_runtime_selection() { use std::io; println!("Choose payment method:"); println!("1. Credit Card"); println!("2. PayPal"); println!("3. Bank Transfer"); // Simulate user input (in real code, you'd read from stdin) let choice = 2; // Simulated input let payment_method = match choice { 1 => PaymentMethod::CreditCard { card_number: "1111-2222-3333-4444".to_string() }, 2 => PaymentMethod::PayPal { email: "customer@email.com".to_string() }, 3 => PaymentMethod::BankTransfer { account_number: "ACC123456789".to_string() }, _ => PaymentMethod::CreditCard { card_number: "DEFAULT-CARD".to_string() }, }; let mut cart = ModernShoppingCart::new(payment_method); cart.add_item("Book".to_string(), 19.99); cart.checkout(); }
Performance Analysis
Static Dispatch (Generics)
- Compile-time optimization: Zero runtime cost for method calls
- Code size: May increase due to monomorphization
- Flexibility: Strategy must be known at compile time
Dynamic Dispatch (Trait Objects)
- Runtime flexibility: Can switch strategies at runtime
- Vtable overhead: Small runtime cost for virtual method calls
- Memory: Additional pointer indirection
Enum-Based Strategy
- Best of both worlds: Pattern matching is optimized by compiler
- Memory efficient: No heap allocation or vtables
- Type safety: All variants known at compile time
Best Practices
- Use enums for closed sets of strategies known at compile time
- Use trait objects for open sets or plugin architectures
- Use generics for maximum performance when strategy is known at compile time
- Consider function pointers for simple strategies without state
When to Use Each Approach
- Enums: When you control all strategies and they're known at compile time
- Trait objects: When you need runtime flexibility or plugin systems
- Generics: When performance is critical and strategy is compile-time known
- Function pointers: For simple stateless strategies
Core Rust concepts introduced
Building on the foundation patterns, the Strategy pattern introduces several new concepts related to dispatch and polymorphism:
1. Trait Objects and Dynamic Dispatch
#![allow(unused)] fn main() { Box<dyn PaymentStrategy> // Trait object &dyn PaymentStrategy // Reference to trait object }
- Trait objects: Allow runtime polymorphism through vtables
dynkeyword: Explicitly marks dynamic dispatch- Object safety: Not all traits can be made into trait objects
- Vtable overhead: Small runtime cost for virtual method calls
2. Static vs Dynamic Dispatch Trade-offs
#![allow(unused)] fn main() { // Static dispatch - zero runtime cost fn process_static<T: PaymentStrategy>(strategy: &T, amount: f64) { strategy.pay(amount); // Compile-time method resolution } // Dynamic dispatch - runtime flexibility fn process_dynamic(strategy: &dyn PaymentStrategy, amount: f64) { strategy.pay(amount); // Runtime method lookup via vtable } }
- Monomorphization: Compiler generates separate code for each concrete type
- Code bloat vs performance: Static dispatch can increase binary size
- Runtime flexibility: Dynamic dispatch allows changing behavior at runtime
3. Object Safety Rules
#![allow(unused)] fn main() { trait ObjectSafe { fn method(&self); // ✅ Object safe } trait NotObjectSafe { fn generic_method<T>(&self); // ❌ Not object safe fn static_method(); // ❌ Not object safe } }
- Object safety requirements: Methods must be callable through trait objects
- No generic methods: Generics prevent trait object creation
- No associated functions: Static methods can't be called on trait objects
- Self limitations:
Selfmust be behind a pointer
4. Box Smart Pointers for Heap Allocation
#![allow(unused)] fn main() { Box<dyn PaymentStrategy> // Owned trait object on heap }
- Heap allocation:
Box<T>allocatesTon the heap - Owned data:
Box<T>owns its data, unlike references - Deref coercion:
Box<T>automatically derefs toT - Zero-cost abstraction: No runtime overhead beyond the allocation
5. Performance Implications of Dispatch Types
Static dispatch characteristics:
- Zero runtime overhead for method calls
- Potential code size increase due to monomorphization
- All optimizations possible (inlining, etc.)
- Strategy must be known at compile time
Dynamic dispatch characteristics:
- Small vtable lookup overhead
- Consistent code size regardless of number of implementers
- Limited optimization opportunities
- Full runtime flexibility
6. Vtables and Runtime Polymorphism
#![allow(unused)] fn main() { // Under the hood, trait objects use vtables struct TraitObject { data: *mut (), // Pointer to the actual data vtable: *const VTable, // Pointer to method implementations } }
- Vtable structure: Contains pointers to trait method implementations
- Double indirection: Data pointer + vtable pointer
- Method resolution: Runtime lookup in vtable for correct implementation
- Memory layout: Understanding the cost of dynamic dispatch
These concepts demonstrate how Rust provides both zero-cost abstractions (static dispatch) and runtime flexibility (dynamic dispatch), allowing you to choose the right trade-off for each situation. The Strategy pattern showcases when and how to use each approach effectively.
Adapter Pattern
The Adapter pattern allows incompatible interfaces to work together by providing a wrapper that translates one interface to another. In Rust, this pattern highlights the orphan rule, coherence principles, and techniques for integrating external types into your type system.
Problem Statement
You need to use a type from an external crate (library) but want to implement a trait for it, or you need to adapt an existing interface to work with your code. Rust's orphan rule prevents implementing external traits for external types, so you need alternative approaches.
Traditional Object-Oriented Implementation
In Java, the Adapter pattern typically wraps an incompatible class:
// Target interface that our code expects
interface MediaPlayer {
void play(String audioType, String fileName);
}
// External library interface (incompatible)
interface AdvancedMediaPlayer {
void playVlc(String fileName);
void playMp4(String fileName);
}
// External library implementation
class VlcPlayer implements AdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
System.out.println("Playing vlc file: " + fileName);
}
@Override
public void playMp4(String fileName) {
// Do nothing - VLC player can't play MP4
}
}
class Mp4Player implements AdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
// Do nothing - MP4 player can't play VLC
}
@Override
public void playMp4(String fileName) {
System.out.println("Playing mp4 file: " + fileName);
}
}
// Adapter that makes external library compatible
class MediaAdapter implements MediaPlayer {
AdvancedMediaPlayer advancedPlayer;
public MediaAdapter(String audioType) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedPlayer = new VlcPlayer();
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedPlayer = new Mp4Player();
}
}
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedPlayer.playVlc(fileName);
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedPlayer.playMp4(fileName);
}
}
}
Understanding the Orphan Rule
Rust's orphan rule (also called the coherence rule) states that you can only implement a trait for a type if you own either the trait or the type. This prevents conflicts but requires adapter patterns:
#![allow(unused)] fn main() { // ❌ This won't compile - orphan rule violation // Can't implement external trait for external type impl std::fmt::Display for std::fs::File { // Error: neither trait nor type is local to this crate } // ❌ This also won't compile // Can't implement external trait for external generic impl<T> std::clone::Clone for Vec<T> { // Error: Vec is from std, Clone is from std } }
Rust Implementation
1. NewType Wrapper Pattern
The most common way to adapt external types:
#![allow(unused)] fn main() { use std::collections::HashMap; // External type we want to adapt type ExternalConfig = HashMap<String, String>; // Our desired trait trait Configurable { fn get_setting(&self, key: &str) -> Option<&str>; fn set_setting(&mut self, key: String, value: String); fn has_setting(&self, key: &str) -> bool; } // NewType wrapper to overcome orphan rule #[derive(Debug, Clone)] struct Config(ExternalConfig); impl Config { fn new() -> Self { Config(HashMap::new()) } fn from_external(external: ExternalConfig) -> Self { Config(external) } fn into_external(self) -> ExternalConfig { self.0 } } // Now we can implement our trait for our wrapper impl Configurable for Config { fn get_setting(&self, key: &str) -> Option<&str> { self.0.get(key).map(String::as_str) } fn set_setting(&mut self, key: String, value: String) { self.0.insert(key, value); } fn has_setting(&self, key: &str) -> bool { self.0.contains_key(key) } } // Implement Deref for convenient access to underlying type impl std::ops::Deref for Config { type Target = ExternalConfig; fn deref(&self) -> &Self::Target { &self.0 } } impl std::ops::DerefMut for Config { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } }
2. Adapter Struct Pattern
When you need to adapt between different interfaces:
#![allow(unused)] fn main() { // Simulating external media library types struct VlcPlayer { volume: u8, } impl VlcPlayer { fn new() -> Self { VlcPlayer { volume: 50 } } fn play_vlc_file(&self, filename: &str) { println!("VLC: Playing {} at volume {}", filename, self.volume); } fn set_volume(&mut self, vol: u8) { self.volume = vol; } } struct Mp4Player { quality: String, } impl Mp4Player { fn new() -> Self { Mp4Player { quality: "HD".to_string() } } fn play_mp4_stream(&self, filename: &str) { println!("MP4: Streaming {} in {} quality", filename, self.quality); } fn set_quality(&mut self, quality: String) { self.quality = quality; } } // Our unified interface trait MediaPlayer { fn play(&self, filename: &str); fn get_info(&self) -> String; } // Adapter for VLC player struct VlcAdapter { player: VlcPlayer, } impl VlcAdapter { fn new() -> Self { VlcAdapter { player: VlcPlayer::new(), } } fn set_volume(&mut self, volume: u8) { self.player.set_volume(volume); } } impl MediaPlayer for VlcAdapter { fn play(&self, filename: &str) { // Adapt the interface self.player.play_vlc_file(filename); } fn get_info(&self) -> String { format!("VLC Player (Volume: {})", self.player.volume) } } // Adapter for MP4 player struct Mp4Adapter { player: Mp4Player, } impl Mp4Adapter { fn new() -> Self { Mp4Adapter { player: Mp4Player::new(), } } fn set_quality(&mut self, quality: String) { self.player.set_quality(quality); } } impl MediaPlayer for Mp4Adapter { fn play(&self, filename: &str) { // Adapt the interface self.player.play_mp4_stream(filename); } fn get_info(&self) -> String { format!("MP4 Player (Quality: {})", self.player.quality) } } }
3. Generic Adapter Pattern
For more flexible adaptations:
#![allow(unused)] fn main() { // External library simulation - temperature sensors trait TemperatureSensor { fn read_celsius(&self) -> f64; } struct AnalogSensor { pin: u8, calibration: f64, } impl AnalogSensor { fn new(pin: u8) -> Self { AnalogSensor { pin, calibration: 1.0 } } fn read_voltage(&self) -> f64 { // Simulate reading from analog pin 3.3 * (self.pin as f64 / 255.0) + self.calibration } } struct DigitalSensor { address: u8, } impl DigitalSensor { fn new(address: u8) -> Self { DigitalSensor { address } } fn read_raw_data(&self) -> u16 { // Simulate I2C communication 1024 + (self.address as u16 * 10) } } // Generic adapter that can adapt any type struct SensorAdapter<T> { sensor: T, conversion_fn: fn(&T) -> f64, } impl<T> SensorAdapter<T> { fn new(sensor: T, conversion_fn: fn(&T) -> f64) -> Self { SensorAdapter { sensor, conversion_fn, } } } impl<T> TemperatureSensor for SensorAdapter<T> { fn read_celsius(&self) -> f64 { (self.conversion_fn)(&self.sensor) } } // Conversion functions fn analog_to_celsius(sensor: &AnalogSensor) -> f64 { let voltage = sensor.read_voltage(); // Convert voltage to temperature (example conversion) (voltage - 0.5) * 100.0 } fn digital_to_celsius(sensor: &DigitalSensor) -> f64 { let raw = sensor.read_raw_data(); // Convert raw digital value to temperature (raw as f64 - 1024.0) / 10.0 } }
4. Trait Object Adapter
For runtime adaptation:
#![allow(unused)] fn main() { // Unified media player using trait objects struct UnifiedMediaPlayer { players: Vec<Box<dyn MediaPlayer>>, current_player: usize, } impl UnifiedMediaPlayer { fn new() -> Self { UnifiedMediaPlayer { players: Vec::new(), current_player: 0, } } fn add_player(&mut self, player: Box<dyn MediaPlayer>) { self.players.push(player); } fn switch_player(&mut self, index: usize) -> Result<(), String> { if index < self.players.len() { self.current_player = index; Ok(()) } else { Err("Player index out of bounds".to_string()) } } fn play(&self, filename: &str) -> Result<(), String> { if let Some(player) = self.players.get(self.current_player) { player.play(filename); Ok(()) } else { Err("No player available".to_string()) } } fn list_players(&self) -> Vec<String> { self.players.iter().map(|p| p.get_info()).collect() } } }
Complete Example and Usage
fn main() { println!("=== Adapter Pattern Examples ===\n"); // 1. NewType wrapper example println!("1. NewType Wrapper Adapter:"); let mut config = Config::new(); config.set_setting("database_url".to_string(), "localhost:5432".to_string()); config.set_setting("max_connections".to_string(), "100".to_string()); println!("Database URL: {:?}", config.get_setting("database_url")); println!("Has timeout setting: {}", config.has_setting("timeout")); // Access underlying HashMap through Deref println!("All settings: {:?}", &*config); // 2. Adapter struct example println!("\n2. Media Player Adapters:"); let vlc = VlcAdapter::new(); let mp4 = Mp4Adapter::new(); vlc.play("movie.vlc"); mp4.play("video.mp4"); println!("Players: {} | {}", vlc.get_info(), mp4.get_info()); // 3. Generic adapter example println!("\n3. Generic Sensor Adapters:"); let analog = AnalogSensor::new(128); let digital = DigitalSensor::new(0x48); let analog_adapter = SensorAdapter::new(analog, analog_to_celsius); let digital_adapter = SensorAdapter::new(digital, digital_to_celsius); println!("Analog sensor: {:.2}°C", analog_adapter.read_celsius()); println!("Digital sensor: {:.2}°C", digital_adapter.read_celsius()); // 4. Trait object adapter example println!("\n4. Unified Media Player:"); let mut unified = UnifiedMediaPlayer::new(); unified.add_player(Box::new(VlcAdapter::new())); unified.add_player(Box::new(Mp4Adapter::new())); println!("Available players: {:?}", unified.list_players()); unified.play("test.vlc").unwrap(); unified.switch_player(1).unwrap(); unified.play("test.mp4").unwrap(); // 5. Demonstrating orphan rule compliance println!("\n5. Working with External Types:"); demonstrate_external_types(); } fn demonstrate_external_types() { use std::path::PathBuf; // We can't implement external traits for external types, // but we can wrap them #[derive(Debug)] struct PathAdapter(PathBuf); impl PathAdapter { fn new<P: Into<PathBuf>>(path: P) -> Self { PathAdapter(path.into()) } } // Now we can implement our own traits trait PathInfo { fn is_source_file(&self) -> bool; fn get_project_relative_path(&self) -> String; } impl PathInfo for PathAdapter { fn is_source_file(&self) -> bool { self.0.extension() .and_then(|ext| ext.to_str()) .map(|ext| matches!(ext, "rs" | "toml" | "md")) .unwrap_or(false) } fn get_project_relative_path(&self) -> String { // Simplified - in real code you'd handle this properly self.0.to_string_lossy().to_string() } } let path = PathAdapter::new("src/main.rs"); println!("Is source file: {}", path.is_source_file()); println!("Relative path: {}", path.get_project_relative_path()); } #[cfg(test)] mod tests { use super::*; #[test] fn test_config_adapter() { let mut config = Config::new(); config.set_setting("key".to_string(), "value".to_string()); assert_eq!(config.get_setting("key"), Some("value")); assert!(config.has_setting("key")); assert!(!config.has_setting("nonexistent")); } #[test] fn test_media_adapters() { let vlc = VlcAdapter::new(); let mp4 = Mp4Adapter::new(); // Just test that the interface works vlc.play("test.vlc"); mp4.play("test.mp4"); assert!(vlc.get_info().contains("VLC")); assert!(mp4.get_info().contains("MP4")); } #[test] fn test_generic_adapter() { let sensor = AnalogSensor::new(100); let adapter = SensorAdapter::new(sensor, analog_to_celsius); let temp = adapter.read_celsius(); assert!(temp > -273.0); // Above absolute zero } }
Orphan Rule Deep Dive
The orphan rule exists to ensure coherence - the property that there's only one implementation of a trait for any given type. This prevents conflicts when multiple crates try to implement the same trait for the same type.
What the Orphan Rule Allows
#![allow(unused)] fn main() { // ✅ Local trait, external type trait MyTrait { fn my_method(&self); } impl MyTrait for String { fn my_method(&self) { println!("My implementation for String"); } } // ✅ External trait, local type struct MyType; impl std::fmt::Display for MyType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "MyType") } } // ✅ Local trait, local type impl MyTrait for MyType { fn my_method(&self) { println!("My implementation for MyType"); } } }
What the Orphan Rule Prevents
#![allow(unused)] fn main() { // ❌ External trait, external type impl std::fmt::Display for std::fs::File { // Error: can't implement external trait for external type } // ❌ Blanket implementation conflict impl<T> std::fmt::Display for Vec<T> { // Error: would conflict with potential upstream implementations } }
Best Practices
- Use NewType wrappers for external types you need to extend
- Implement
DerefandDerefMutfor transparent access to wrapped types - Consider generic adapters for families of similar types
- Use composition over inheritance - wrap rather than extend
- Document the adaptation clearly for maintainers
When to Use the Adapter Pattern
- Integrating external libraries with incompatible interfaces
- Gradually migrating from one interface to another
- Creating facades for complex external APIs
- Adding functionality to types you don't own
- Ensuring interface consistency across different implementations
Core Rust concepts introduced
Building on previous patterns, the Adapter pattern introduces several new concepts related to Rust's coherence rules and type system integration:
1. The Orphan Rule and Coherence
#![allow(unused)] fn main() { // ❌ Orphan rule violation impl std::fmt::Display for std::fs::File { // Error: neither trait nor type is local to this crate } // ✅ Orphan rule compliance - wrap external type struct FileAdapter(std::fs::File); impl std::fmt::Display for FileAdapter { // OK: local type implementing external trait } }
- Coherence principle: Ensures only one implementation of a trait for any type
- Orphan rule: Can only implement trait for type if you own either the trait or the type
- Conflict prevention: Prevents diamond problem and implementation conflicts
- Upstream/downstream safety: Protects against breaking changes from dependencies
2. NewType Wrapper for External Types
#![allow(unused)] fn main() { #[derive(Debug, Clone)] struct Config(HashMap<String, String>); // Wrapper around external type impl Deref for Config { type Target = HashMap<String, String>; fn deref(&self) -> &Self::Target { &self.0 } } }
- Single-field tuple struct: Wraps external type in local type
- Zero-cost abstraction: Wrapper has no runtime overhead
- Deref coercion: Provides transparent access to wrapped type
- API control: You choose which methods to expose
3. Foreign Function Interface (FFI) Considerations
#![allow(unused)] fn main() { // When adapting C libraries or external bindings #[repr(C)] struct CStruct { field: i32, } struct SafeWrapper(CStruct); impl SafeWrapper { fn new(value: i32) -> Self { SafeWrapper(CStruct { field: value }) } // Provide safe interface to unsafe operations fn get_value(&self) -> i32 { self.0.field } } }
- Memory layout control:
#[repr(C)]for C compatibility - Safety boundary: Wrapper provides safe interface to unsafe operations
- Resource management: Wrapper can handle cleanup of external resources
- Type safety: Convert between Rust and external type systems
4. Trait Implementation Restrictions
#![allow(unused)] fn main() { // These restrictions exist due to coherence rules: // ❌ Can't add blanket implementations that might conflict impl<T> Clone for Vec<T> { // Error: might conflict with std's implementation } // ✅ Can implement for specific types you control impl Clone for MyWrapper<SomeType> { // OK: MyWrapper is local to this crate } }
- Blanket implementation conflicts: Generic implementations can create conflicts
- Specialization limitations: Rust prevents overlapping implementations
- Future compatibility: Rules protect against breaking changes in dependencies
- Explicit is better: Forces explicit wrapper types for clarity
5. Upstream/Downstream Crate Relationships
#![allow(unused)] fn main() { // Understanding crate dependencies for orphan rule // In your crate (downstream from std): use std::collections::HashMap; // ❌ Can't implement std trait for std type impl std::fmt::Display for HashMap<String, String> { // Error: both trait and type are upstream } // ✅ Can wrap and implement struct DisplayableMap(HashMap<String, String>); impl std::fmt::Display for DisplayableMap { // OK: local type, external trait } }
- Upstream crates: Dependencies your crate relies on
- Downstream crates: Crates that depend on your crate
- Orphan rule direction: Protects upstream crates from downstream changes
- Semantic versioning: Supports SemVer by preventing breaking changes
6. Blanket Implementations and Conflicts
#![allow(unused)] fn main() { // Understanding how blanket implementations create restrictions // In std library: impl<T: Clone> Clone for Vec<T> { // This prevents you from implementing Clone for Vec<YourType> } // Your code: // ❌ This would conflict with std's blanket implementation impl Clone for Vec<MyType> { // Error: conflicting implementation } // ✅ Use newtype to avoid conflict struct MyVec<T>(Vec<T>); impl<T: Clone> Clone for MyVec<T> { // OK: different type } }
- Blanket implementations: Generic implementations that cover many types
- Conflict detection: Rust prevents potentially overlapping implementations
- Coherence checking: Compiler ensures only one implementation path exists
- Workaround patterns: NewType is the standard solution
These concepts demonstrate how Rust's type system maintains safety and predictability while providing escape hatches through adapter patterns. The orphan rule might seem restrictive, but it prevents entire classes of bugs and versioning problems common in other languages.
Interior Mutability Pattern
Interior mutability is a design pattern in Rust that allows you to mutate data even when there are immutable references to it. This pattern is essential for scenarios where the borrow checker's compile-time restrictions are too conservative, but you can guarantee safety through runtime checks.
Problem Statement
Sometimes you need to mutate data through shared references, or you have complex borrowing patterns that the borrow checker can't statically verify as safe. Traditional Rust borrowing requires exclusive access for mutation, but certain patterns require mutation through shared ownership.
Understanding Inherited vs Interior Mutability
#![allow(unused)] fn main() { // Inherited mutability - mutability "inherited" from binding let mut x = 5; x = 10; // ✅ OK - x is mutable let y = 5; // y = 10; // ❌ Error - y is immutable // Interior mutability - mutation through immutable references use std::cell::Cell; let x = Cell::new(5); // x itself is immutable x.set(10); // ✅ OK - interior mutability allows this }
When Interior Mutability is Needed
- Shared ownership with mutation - Multiple owners need to modify data
- Caching and memoization - Immutable interface with internal optimization
- Observer patterns - Notifying observers requires mutation
- Circular references - Breaking cycles while maintaining mutability
- C FFI - Interfacing with C code that expects mutable data
Types of Interior Mutability
1. Cell<T> - For Copy Types
Cell<T> provides interior mutability for Copy types through value replacement:
#![allow(unused)] fn main() { use std::cell::Cell; struct Counter { value: Cell<u32>, name: String, } impl Counter { fn new(name: String) -> Self { Counter { value: Cell::new(0), name, } } // Can increment through immutable reference fn increment(&self) { let current = self.value.get(); self.value.set(current + 1); } fn get(&self) -> u32 { self.value.get() } fn reset(&self) { self.value.set(0); } } fn demonstrate_cell() { let counter = Counter::new("events".to_string()); // All these work through immutable references counter.increment(); counter.increment(); println!("{}: {}", counter.name, counter.get()); // events: 2 // Multiple immutable references can all mutate let counter_ref1 = &counter; let counter_ref2 = &counter; counter_ref1.increment(); counter_ref2.increment(); println!("{}: {}", counter.name, counter.get()); // events: 4 } }
2. RefCell<T> - For Runtime Borrow Checking
RefCell<T> provides interior mutability for any type through runtime borrow checking:
#![allow(unused)] fn main() { use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] struct Node { value: i32, children: RefCell<Vec<Rc<Node>>>, parent: RefCell<Option<Rc<Node>>>, } impl Node { fn new(value: i32) -> Rc<Self> { Rc::new(Node { value, children: RefCell::new(Vec::new()), parent: RefCell::new(None), }) } fn add_child(self: &Rc<Self>, child: Rc<Node>) { // Mutate through shared reference self.children.borrow_mut().push(child.clone()); *child.parent.borrow_mut() = Some(self.clone()); } fn remove_child(self: &Rc<Self>, child_value: i32) -> bool { let mut children = self.children.borrow_mut(); if let Some(pos) = children.iter().position(|child| child.value == child_value) { let removed = children.remove(pos); *removed.parent.borrow_mut() = None; true } else { false } } fn get_children_values(&self) -> Vec<i32> { self.children.borrow().iter().map(|child| child.value).collect() } } fn demonstrate_refcell() { let root = Node::new(1); let child1 = Node::new(2); let child2 = Node::new(3); root.add_child(child1); root.add_child(child2); println!("Root children: {:?}", root.get_children_values()); // [2, 3] root.remove_child(2); println!("After removal: {:?}", root.get_children_values()); // [3] } }
3. Mutex<T> - For Thread-Safe Interior Mutability
Mutex<T> provides thread-safe interior mutability:
#![allow(unused)] fn main() { use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; struct SharedCounter { value: Mutex<u32>, name: String, } impl SharedCounter { fn new(name: String) -> Arc<Self> { Arc::new(SharedCounter { value: Mutex::new(0), name, }) } fn increment(&self) -> Result<(), String> { match self.value.lock() { Ok(mut guard) => { *guard += 1; Ok(()) } Err(_) => Err("Mutex poisoned".to_string()), } } fn get(&self) -> Result<u32, String> { match self.value.lock() { Ok(guard) => Ok(*guard), Err(_) => Err("Mutex poisoned".to_string()), } } fn add(&self, amount: u32) -> Result<(), String> { match self.value.lock() { Ok(mut guard) => { *guard += amount; Ok(()) } Err(_) => Err("Mutex poisoned".to_string()), } } } fn demonstrate_mutex() { let counter = SharedCounter::new("thread_safe".to_string()); let mut handles = vec![]; // Spawn multiple threads that all increment the counter for i in 0..5 { let counter_clone = counter.clone(); let handle = thread::spawn(move || { for _ in 0..10 { counter_clone.increment().unwrap(); thread::sleep(Duration::from_millis(1)); } println!("Thread {} finished", i); }); handles.push(handle); } // Wait for all threads to complete for handle in handles { handle.join().unwrap(); } println!("Final count: {}", counter.get().unwrap()); // Should be 50 } }
4. RwLock<T> - For Reader-Writer Patterns
RwLock<T> allows multiple readers or one writer:
#![allow(unused)] fn main() { use std::sync::{Arc, RwLock}; use std::thread; use std::collections::HashMap; struct SharedCache { data: RwLock<HashMap<String, String>>, } impl SharedCache { fn new() -> Arc<Self> { Arc::new(SharedCache { data: RwLock::new(HashMap::new()), }) } fn get(&self, key: &str) -> Option<String> { // Multiple readers can access simultaneously self.data.read().unwrap().get(key).cloned() } fn insert(&self, key: String, value: String) { // Exclusive writer access self.data.write().unwrap().insert(key, value); } fn remove(&self, key: &str) -> Option<String> { self.data.write().unwrap().remove(key) } fn len(&self) -> usize { self.data.read().unwrap().len() } } fn demonstrate_rwlock() { let cache = SharedCache::new(); // Insert some initial data cache.insert("user:1".to_string(), "Alice".to_string()); cache.insert("user:2".to_string(), "Bob".to_string()); let mut handles = vec![]; // Spawn reader threads for i in 0..3 { let cache_clone = cache.clone(); let handle = thread::spawn(move || { for j in 0..5 { let key = format!("user:{}", (j % 2) + 1); if let Some(value) = cache_clone.get(&key) { println!("Reader {}: {} = {}", i, key, value); } thread::sleep(Duration::from_millis(10)); } }); handles.push(handle); } // Spawn writer thread let cache_clone = cache.clone(); let writer_handle = thread::spawn(move || { for i in 3..6 { let key = format!("user:{}", i); let value = format!("User{}", i); cache_clone.insert(key.clone(), value.clone()); println!("Writer: inserted {} = {}", key, value); thread::sleep(Duration::from_millis(50)); } }); // Wait for all threads for handle in handles { handle.join().unwrap(); } writer_handle.join().unwrap(); println!("Final cache size: {}", cache.len()); } }
Advanced Patterns
1. Lazy Initialization with OnceCell
#![allow(unused)] fn main() { use std::cell::OnceCell; use std::sync::OnceLock; struct LazyData { expensive_computation: OnceCell<String>, } impl LazyData { fn new() -> Self { LazyData { expensive_computation: OnceCell::new(), } } fn get_data(&self) -> &str { self.expensive_computation.get_or_init(|| { println!("Performing expensive computation..."); thread::sleep(Duration::from_millis(100)); "Computed result".to_string() }) } } // Thread-safe lazy initialization static GLOBAL_DATA: OnceLock<String> = OnceLock::new(); fn get_global_data() -> &'static str { GLOBAL_DATA.get_or_init(|| { println!("Initializing global data..."); "Global computed result".to_string() }) } }
2. Cache with Interior Mutability
#![allow(unused)] fn main() { use std::cell::RefCell; use std::collections::HashMap; struct MemoizedFunction<F, K, V> where F: Fn(&K) -> V, K: Clone + Eq + std::hash::Hash, V: Clone, { function: F, cache: RefCell<HashMap<K, V>>, } impl<F, K, V> MemoizedFunction<F, K, V> where F: Fn(&K) -> V, K: Clone + Eq + std::hash::Hash, V: Clone, { fn new(function: F) -> Self { MemoizedFunction { function, cache: RefCell::new(HashMap::new()), } } fn call(&self, key: &K) -> V { // Try to get from cache first if let Some(cached) = self.cache.borrow().get(key) { return cached.clone(); } // Compute and cache result let result = (self.function)(key); self.cache.borrow_mut().insert(key.clone(), result.clone()); result } fn clear_cache(&self) { self.cache.borrow_mut().clear(); } } fn demonstrate_memoization() { let expensive_fn = |n: &u32| -> u64 { println!("Computing fibonacci({})...", n); thread::sleep(Duration::from_millis(100)); // Simple fibonacci (inefficient on purpose) match *n { 0 => 0, 1 => 1, n => { let mut a = 0; let mut b = 1; for _ in 2..=n { let temp = a + b; a = b; b = temp; } b } } }; let memoized = MemoizedFunction::new(expensive_fn); // First call - computes println!("First call: {}", memoized.call(&10)); // Second call - cached println!("Second call: {}", memoized.call(&10)); // Different input - computes println!("Different input: {}", memoized.call(&15)); } }
Complete Example and Usage
fn main() { println!("=== Interior Mutability Pattern Examples ===\n"); // 1. Cell example println!("1. Cell<T> for Copy types:"); demonstrate_cell(); // 2. RefCell example println!("\n2. RefCell<T> for runtime borrow checking:"); demonstrate_refcell(); // 3. Mutex example println!("\n3. Mutex<T> for thread-safe interior mutability:"); demonstrate_mutex(); // 4. RwLock example println!("\n4. RwLock<T> for reader-writer patterns:"); demonstrate_rwlock(); // 5. Lazy initialization println!("\n5. Lazy initialization with OnceCell:"); let lazy = LazyData::new(); println!("First access: {}", lazy.get_data()); println!("Second access: {}", lazy.get_data()); println!("Global data: {}", get_global_data()); println!("Global data again: {}", get_global_data()); // 6. Memoization example println!("\n6. Memoization with RefCell:"); demonstrate_memoization(); // 7. Error handling with interior mutability println!("\n7. Error handling:"); demonstrate_error_handling(); } fn demonstrate_error_handling() { use std::cell::RefCell; let data = RefCell::new(vec![1, 2, 3]); // This will panic - demonstrates runtime borrow checking // let _borrow1 = data.borrow_mut(); // let _borrow2 = data.borrow_mut(); // Panic: already borrowed // Safe approach - check before borrowing match data.try_borrow_mut() { Ok(mut borrowed) => { borrowed.push(4); println!("Successfully modified: {:?}", *borrowed); } Err(e) => { println!("Failed to borrow: {}", e); } } // Demonstrate proper scoping { let mut borrowed = data.borrow_mut(); borrowed.push(5); } // Borrow released here // Now we can borrow again println!("Final data: {:?}", data.borrow()); } #[cfg(test)] mod tests { use super::*; #[test] fn test_cell_basic_operations() { let cell = Cell::new(42); assert_eq!(cell.get(), 42); cell.set(100); assert_eq!(cell.get(), 100); } #[test] fn test_refcell_borrowing() { let refcell = RefCell::new(vec![1, 2, 3]); // Immutable borrow { let borrowed = refcell.borrow(); assert_eq!(borrowed.len(), 3); } // Mutable borrow { let mut borrowed = refcell.borrow_mut(); borrowed.push(4); } assert_eq!(refcell.borrow().len(), 4); } #[test] #[should_panic(expected = "already borrowed")] fn test_refcell_panic_on_multiple_mut_borrow() { let refcell = RefCell::new(42); let _borrow1 = refcell.borrow_mut(); let _borrow2 = refcell.borrow_mut(); // This should panic } #[test] fn test_try_borrow() { let refcell = RefCell::new(42); let _borrow1 = refcell.borrow_mut(); // try_borrow should fail gracefully assert!(refcell.try_borrow().is_err()); assert!(refcell.try_borrow_mut().is_err()); } }
Performance Characteristics
| Type | Runtime Check | Thread Safe | Use Case |
|---|---|---|---|
Cell<T> | None | No | Copy types, simple mutation |
RefCell<T> | Borrow checking | No | Complex types, single thread |
Mutex<T> | Lock contention | Yes | Multi-threaded mutation |
RwLock<T> | Reader/writer locks | Yes | Many readers, few writers |
Best Practices
- Prefer compile-time borrowing when possible
- Use
Cell<T>for simple Copy types - it's the most efficient - Use
RefCell<T>sparingly - it moves errors from compile-time to runtime - Handle
RefCellpanics gracefully withtry_borrowmethods - Use
Mutex<T>only when you need thread safety - it has overhead - Consider
RwLock<T>for read-heavy workloads - Minimize the scope of borrows to avoid conflicts
When to Use Interior Mutability
Use interior mutability when:
- You need shared ownership with mutation (
Rc<RefCell<T>>) - Implementing caches or memoization
- Working with observer patterns
- Interfacing with C code that expects mutable data
- The borrow checker is too conservative for your use case
Avoid interior mutability when:
- Normal borrowing rules work fine
- You can restructure your code to avoid the need
- Performance is critical (runtime checks have cost)
- You want compile-time guarantees
Core Rust concepts introduced
Building on previous patterns, the Interior Mutability pattern introduces several new concepts about runtime safety and shared mutation:
1. Interior Mutability vs Inherited Mutability
#![allow(unused)] fn main() { // Inherited mutability - from the binding let mut x = 5; x = 10; // OK because binding is mutable // Interior mutability - through the type let x = Cell::new(5); // binding is immutable x.set(10); // OK because Cell provides interior mutability }
- Inherited mutability: Mutability comes from the variable binding (
let mut) - Interior mutability: Type allows mutation through immutable references
- Orthogonal concepts: Interior mutability works regardless of binding mutability
- Safety trade-off: Moves some checks from compile-time to runtime
2. Cell<T> for Copy Types
#![allow(unused)] fn main() { use std::cell::Cell; let counter = Cell::new(0_u32); counter.set(counter.get() + 1); // Replace entire value }
- Copy types only:
Cell<T>requiresT: Copy - Value replacement: Can only get/set entire values, not references
- Zero runtime cost: No additional overhead beyond the operation
- Thread-local only: Not thread-safe, but very efficient
3. RefCell<T> for Runtime Borrow Checking
#![allow(unused)] fn main() { use std::cell::RefCell; let data = RefCell::new(vec![1, 2, 3]); let borrowed = data.borrow_mut(); // Runtime borrow check }
- Runtime borrow checking: Same rules as compile-time, but checked at runtime
- Dynamic borrowing:
borrow()andborrow_mut()return smart pointers - Panic on violation: Runtime panic if borrow rules are violated
try_borrowmethods: Non-panicking variants that returnResult
4. Mutex<T> for Thread-Safe Interior Mutability
#![allow(unused)] fn main() { use std::sync::Mutex; let data = Mutex::new(42); let mut guard = data.lock().unwrap(); // Acquire lock *guard = 100; // Mutate through guard }
- Thread-safe mutation: Can be shared across threads
- Lock acquisition:
lock()blocks until lock is available - Lock guards: RAII guards automatically release locks
- Poisoning: Mutex becomes "poisoned" if a thread panics while holding the lock
5. Runtime vs Compile-Time Borrow Checking
#![allow(unused)] fn main() { // Compile-time checking fn compile_time(data: &mut Vec<i32>) { data.push(1); // ✅ Checked at compile time } // Runtime checking fn runtime_checking(data: &RefCell<Vec<i32>>) { let mut borrowed = data.borrow_mut(); // ✅ Checked at runtime borrowed.push(1); // let another = data.borrow_mut(); // ❌ Would panic at runtime } }
- Compile-time: Zero runtime cost, but less flexible
- Runtime: More flexible, but potential for runtime panics
- Error discovery: Compile-time errors vs runtime panics
- Performance impact: Runtime checks have small overhead
6. Shared Ownership with Rc<RefCell<T>>
#![allow(unused)] fn main() { use std::rc::Rc; use std::cell::RefCell; let shared_data = Rc::new(RefCell::new(vec![1, 2, 3])); let clone1 = shared_data.clone(); // Reference counting let clone2 = shared_data.clone(); // Multiple owners can all mutate the data clone1.borrow_mut().push(4); clone2.borrow_mut().push(5); }
- Reference counting:
Rc<T>provides shared ownership - Interior mutability:
RefCell<T>allows mutation through shared references - Common pattern:
Rc<RefCell<T>>for shared mutable data in single-threaded contexts - Thread-safe alternative:
Arc<Mutex<T>>for multi-threaded scenarios
7. Memory Safety Trade-offs
#![allow(unused)] fn main() { // Compile-time safety let mut data = vec![1, 2, 3]; let reference = &data[0]; // data.push(4); // ❌ Compile error: can't mutate while borrowed // Runtime safety let data = RefCell::new(vec![1, 2, 3]); let reference = data.borrow(); let first = reference[0]; // let mut mutable = data.borrow_mut(); // ❌ Runtime panic: already borrowed }
- Compile-time guarantees: Impossible to violate borrowing rules
- Runtime flexibility: Can implement patterns impossible with compile-time checks
- Error timing: Compile errors vs runtime panics
- Performance: Zero-cost vs small runtime overhead
These concepts show how Rust provides escape hatches from its strict compile-time borrowing rules while maintaining memory safety through runtime checks. Interior mutability is a powerful tool that should be used judiciously - prefer compile-time safety when possible, but don't hesitate to use interior mutability when the flexibility is genuinely needed.
Cow (Clone on Write) Pattern
Clone on Write (Cow) is a optimization pattern that defers cloning expensive data until mutation is actually needed. Rust's Cow<'a, T> type provides this functionality, allowing functions to accept both owned and borrowed data while only cloning when necessary.
Problem Statement
You want to write functions that can accept both owned and borrowed data, and you want to avoid unnecessary cloning. Sometimes you need to return data that might be a reference to input data (zero-copy) or newly allocated data (when modification is needed).
Understanding Clone on Write
The core idea is simple: keep a reference to data as long as possible, only clone when you need to modify it.
#![allow(unused)] fn main() { use std::borrow::Cow; // Without Cow - always clones fn process_string_clone(input: &str) -> String { let mut result = input.to_string(); // Always clones if input.contains("ERROR") { result = result.replace("ERROR", "WARNING"); } result } // With Cow - clones only when needed fn process_string_cow(input: &str) -> Cow<str> { if input.contains("ERROR") { Cow::Owned(input.replace("ERROR", "WARNING")) // Clone only when modifying } else { Cow::Borrowed(input) // No cloning needed } } }
Cow<'a, T> Enum Structure
#![allow(unused)] fn main() { pub enum Cow<'a, T: ?Sized + ToOwned> { Borrowed(&'a T), Owned(<T as ToOwned>::Owned), } }
The Cow enum has two variants:
Borrowed(&'a T)- holds a reference to existing dataOwned(T::Owned)- holds owned data
Basic Cow Usage
String Processing Examples
#![allow(unused)] fn main() { use std::borrow::Cow; fn remove_whitespace(input: &str) -> Cow<str> { if input.contains(char::is_whitespace) { // Need to modify - create owned string Cow::Owned(input.chars().filter(|c| !c.is_whitespace()).collect()) } else { // No modification needed - return borrowed Cow::Borrowed(input) } } fn add_prefix(input: &str, prefix: &str) -> Cow<str> { if input.starts_with(prefix) { Cow::Borrowed(input) // Already has prefix } else { Cow::Owned(format!("{}{}", prefix, input)) // Need to add prefix } } fn normalize_case(input: &str) -> Cow<str> { if input.chars().all(|c| c.is_lowercase()) { Cow::Borrowed(input) // Already lowercase } else { Cow::Owned(input.to_lowercase()) // Convert to lowercase } } fn demonstrate_string_processing() { println!("=== String Processing with Cow ==="); let text1 = "hello world"; let text2 = "hello world with spaces"; println!("Original: '{}'", text1); match remove_whitespace(text1) { Cow::Borrowed(s) => println!("No whitespace (borrowed): '{}'", s), Cow::Owned(s) => println!("Whitespace removed (owned): '{}'", s), } println!("Original: '{}'", text2); match remove_whitespace(text2) { Cow::Borrowed(s) => println!("No whitespace (borrowed): '{}'", s), Cow::Owned(s) => println!("Whitespace removed (owned): '{}'", s), } // Prefix examples let url1 = "https://example.com"; let url2 = "example.com"; println!("URL1: {} -> {}", url1, add_prefix(url1, "https://")); println!("URL2: {} -> {}", url2, add_prefix(url2, "https://")); } }
Path Processing
#![allow(unused)] fn main() { use std::borrow::Cow; use std::path::Path; fn normalize_path(path: &Path) -> Cow<Path> { // Check if path needs normalization let path_str = path.to_string_lossy(); if path_str.contains("//") || path_str.contains("/./") || path_str.contains("/../") { // Need to normalize - create owned PathBuf let mut components = Vec::new(); for component in path.components() { match component { std::path::Component::CurDir => { // Skip current directory "." } std::path::Component::ParentDir => { // Handle parent directory ".." components.pop(); } other => { components.push(other); } } } let mut normalized = std::path::PathBuf::new(); for component in components { normalized.push(component); } Cow::Owned(normalized) } else { // Path is already normalized Cow::Borrowed(path) } } fn ensure_extension(path: &Path, ext: &str) -> Cow<Path> { if path.extension().and_then(|e| e.to_str()) == Some(ext) { Cow::Borrowed(path) } else { let mut owned = path.to_path_buf(); owned.set_extension(ext); Cow::Owned(owned) } } fn demonstrate_path_processing() { println!("\n=== Path Processing with Cow ==="); let paths = [ Path::new("/home/user/documents"), Path::new("/home/user//documents/./file.txt"), Path::new("/home/user/documents/../downloads/file.txt"), ]; for path in &paths { println!("Original: {:?}", path); match normalize_path(path) { Cow::Borrowed(p) => println!(" Already normalized (borrowed): {:?}", p), Cow::Owned(p) => println!(" Normalized (owned): {:?}", p), } } // Extension examples let file1 = Path::new("document.txt"); let file2 = Path::new("document"); println!("File1: {:?} -> {:?}", file1, ensure_extension(file1, "txt")); println!("File2: {:?} -> {:?}", file2, ensure_extension(file2, "txt")); } }
Advanced Cow Patterns
Configuration Processing
#![allow(unused)] fn main() { use std::borrow::Cow; use std::collections::HashMap; #[derive(Debug, Clone)] struct Config { settings: HashMap<String, String>, } impl Config { fn new() -> Self { Config { settings: HashMap::new(), } } fn get_or_default<'a>(&'a self, key: &str, default: &'a str) -> Cow<'a, str> { self.settings.get(key) .map(|s| Cow::Borrowed(s.as_str())) .unwrap_or(Cow::Borrowed(default)) } fn get_processed<'a>(&'a self, key: &str) -> Option<Cow<'a, str>> { self.settings.get(key).map(|value| { if value.starts_with('$') { // Environment variable substitution let var_name = &value[1..]; match std::env::var(var_name) { Ok(env_value) => Cow::Owned(env_value), Err(_) => Cow::Borrowed(value.as_str()), } } else { Cow::Borrowed(value.as_str()) } }) } } fn demonstrate_config() { println!("\n=== Configuration Processing ==="); let mut config = Config::new(); config.settings.insert("host".to_string(), "localhost".to_string()); config.settings.insert("port".to_string(), "8080".to_string()); config.settings.insert("env_path".to_string(), "$PATH".to_string()); // Get with defaults println!("Host: {}", config.get_or_default("host", "0.0.0.0")); println!("Timeout: {}", config.get_or_default("timeout", "30")); // Process environment variables if let Some(path) = config.get_processed("env_path") { match path { Cow::Borrowed(s) => println!("Path (borrowed): {}", s), Cow::Owned(s) => println!("Path (from env): {}", &s[..50.min(s.len())]), } } } }
Data Transformation Pipeline
#![allow(unused)] fn main() { use std::borrow::Cow; trait Transform { fn transform<'a>(&self, input: Cow<'a, str>) -> Cow<'a, str>; } struct TrimTransform; impl Transform for TrimTransform { fn transform<'a>(&self, input: Cow<'a, str>) -> Cow<'a, str> { let trimmed = input.trim(); if trimmed.len() == input.len() { input // No change needed } else { Cow::Owned(trimmed.to_string()) } } } struct UppercaseTransform; impl Transform for UppercaseTransform { fn transform<'a>(&self, input: Cow<'a, str>) -> Cow<'a, str> { if input.chars().all(|c| c.is_uppercase() || !c.is_alphabetic()) { input // Already uppercase } else { Cow::Owned(input.to_uppercase()) } } } struct ReplaceTransform { from: String, to: String, } impl ReplaceTransform { fn new(from: &str, to: &str) -> Self { ReplaceTransform { from: from.to_string(), to: to.to_string(), } } } impl Transform for ReplaceTransform { fn transform<'a>(&self, input: Cow<'a, str>) -> Cow<'a, str> { if input.contains(&self.from) { Cow::Owned(input.replace(&self.from, &self.to)) } else { input } } } struct Pipeline { transforms: Vec<Box<dyn Transform>>, } impl Pipeline { fn new() -> Self { Pipeline { transforms: Vec::new(), } } fn add_transform(mut self, transform: Box<dyn Transform>) -> Self { self.transforms.push(transform); self } fn process<'a>(&self, input: &'a str) -> Cow<'a, str> { let mut current = Cow::Borrowed(input); for transform in &self.transforms { current = transform.transform(current); } current } } fn demonstrate_pipeline() { println!("\n=== Transformation Pipeline ==="); let pipeline = Pipeline::new() .add_transform(Box::new(TrimTransform)) .add_transform(Box::new(ReplaceTransform::new("error", "warning"))) .add_transform(Box::new(UppercaseTransform)); let inputs = [ " hello world ", "ALREADY UPPERCASE", " this has an error message ", "clean text", ]; for input in &inputs { println!("Input: '{}'", input); match pipeline.process(input) { Cow::Borrowed(s) => println!(" Output (borrowed): '{}'", s), Cow::Owned(s) => println!(" Output (owned): '{}'", s), } } } }
Custom ToOwned Implementation
#![allow(unused)] fn main() { use std::borrow::{Cow, ToOwned}; // Custom type that can be borrowed/owned #[derive(Debug, Clone, PartialEq)] struct CustomData { values: Vec<i32>, } // Borrowed version #[derive(Debug, PartialEq)] struct CustomDataRef<'a> { values: &'a [i32], } impl ToOwned for CustomDataRef<'_> { type Owned = CustomData; fn to_owned(&self) -> Self::Owned { CustomData { values: self.values.to_vec(), } } } impl<'a> From<&'a CustomData> for CustomDataRef<'a> { fn from(data: &'a CustomData) -> Self { CustomDataRef { values: &data.values, } } } fn process_custom_data(input: CustomDataRef) -> Cow<CustomDataRef> { // Only clone if we need to modify if input.values.iter().any(|&x| x < 0) { // Need to filter out negative numbers let filtered: Vec<i32> = input.values.iter().filter(|&&x| x >= 0).copied().collect(); let owned = CustomData { values: filtered }; Cow::Owned(owned) } else { Cow::Borrowed(input) } } fn demonstrate_custom_toowned() { println!("\n=== Custom ToOwned Implementation ==="); let data1 = CustomData { values: vec![1, 2, 3, 4, 5] }; let data2 = CustomData { values: vec![1, -2, 3, -4, 5] }; let ref1 = CustomDataRef::from(&data1); let ref2 = CustomDataRef::from(&data2); match process_custom_data(ref1) { Cow::Borrowed(r) => println!("No negatives (borrowed): {:?}", r), Cow::Owned(o) => println!("Filtered (owned): {:?}", o), } match process_custom_data(ref2) { Cow::Borrowed(r) => println!("No negatives (borrowed): {:?}", r), Cow::Owned(o) => println!("Filtered (owned): {:?}", o), } } }
Complete Example and Usage
fn main() { println!("=== Cow (Clone on Write) Pattern Examples ==="); // Basic string processing demonstrate_string_processing(); // Path processing demonstrate_path_processing(); // Configuration processing demonstrate_config(); // Transformation pipeline demonstrate_pipeline(); // Custom ToOwned demonstrate_custom_toowned(); // Performance comparison demonstrate_performance(); } fn demonstrate_performance() { println!("\n=== Performance Comparison ==="); let large_string = "a".repeat(1_000_000); let iterations = 1000; // Measure always-cloning approach let start = std::time::Instant::now(); for _ in 0..iterations { let _result = always_clone(&large_string); } let always_clone_time = start.elapsed(); // Measure cow approach (no modification needed) let start = std::time::Instant::now(); for _ in 0..iterations { let _result = cow_approach(&large_string); } let cow_time = start.elapsed(); println!("Always clone: {:?}", always_clone_time); println!("Cow (no modification): {:?}", cow_time); println!("Speedup: {:.2}x", always_clone_time.as_secs_f64() / cow_time.as_secs_f64()); } fn always_clone(input: &str) -> String { let result = input.to_string(); // Always clone if result.contains("xyz") { result.replace("xyz", "abc") } else { result } } fn cow_approach(input: &str) -> Cow<str> { if input.contains("xyz") { Cow::Owned(input.replace("xyz", "abc")) } else { Cow::Borrowed(input) // No clone needed } } #[cfg(test)] mod tests { use super::*; #[test] fn test_cow_borrowed_case() { let input = "hello world"; let result = remove_whitespace(input); match result { Cow::Borrowed(s) => assert_eq!(s, "hello world"), Cow::Owned(_) => panic!("Expected borrowed, got owned"), } } #[test] fn test_cow_owned_case() { let input = "hello world"; let result = remove_whitespace(input); match result { Cow::Borrowed(_) => panic!("Expected owned, got borrowed"), Cow::Owned(s) => assert_eq!(s, "helloworld"), } } #[test] fn test_prefix_addition() { assert_eq!(add_prefix("https://example.com", "https://"), "https://example.com"); assert_eq!(add_prefix("example.com", "https://"), "https://example.com"); } #[test] fn test_transformation_pipeline() { let pipeline = Pipeline::new() .add_transform(Box::new(TrimTransform)) .add_transform(Box::new(UppercaseTransform)); let result = pipeline.process(" hello "); assert_eq!(result, "HELLO"); } }
When to Use Cow
Use Cow when:
- You frequently don't need to modify input data
- You want to avoid unnecessary allocations
- You're building APIs that should accept both owned and borrowed data
- You're implementing transformation pipelines
- You need to conditionally modify data
Don't use Cow when:
- You always need to modify the data
- The data is small and cloning is cheap
- The complexity isn't worth the optimization
- You're working with non-cloneable data
Performance Characteristics
- Best case: Zero allocations when no modification is needed
- Worst case: Same cost as always cloning
- Memory: Only uses extra memory when cloning is necessary
- CPU: Small overhead for the enum check, big savings when avoiding clones
API Design with Cow
#![allow(unused)] fn main() { // Good: Flexible API that accepts both owned and borrowed fn process_text(input: impl Into<Cow<str>>) -> Cow<str> { let text = input.into(); // Process the text... text } // Usage: process_text("borrowed string"); // &str process_text(String::from("owned")); // String process_text(Cow::Borrowed("explicit")); // Explicit Cow }
Core Rust concepts introduced
Building on previous patterns, the Cow pattern introduces several new concepts related to memory efficiency and flexible APIs:
1. Cow<'a, T> Enum and Borrowed/Owned Variants
#![allow(unused)] fn main() { pub enum Cow<'a, T: ?Sized + ToOwned> { Borrowed(&'a T), // Reference to existing data Owned(<T as ToOwned>::Owned), // Owned copy of the data } }
- Delayed cloning: Only clone when mutation is actually needed
- Lifetime parameter:
'atracks how long borrowed data is valid - Type flexibility: Can hold either borrowed or owned data of the same logical type
- Pattern matching: Use
matchto handle both cases explicitly
2. Lazy Cloning and Performance Optimization
#![allow(unused)] fn main() { fn process_data(input: &str) -> Cow<str> { if needs_modification(input) { Cow::Owned(expensive_transformation(input)) // Clone only when needed } else { Cow::Borrowed(input) // Zero-cost when no modification needed } } }
- Conditional cloning: Only allocate memory when modification is required
- Best-case zero-cost: No allocation when data doesn't need changes
- Worst-case equivalent: Same cost as always cloning when modification is needed
- Memory efficiency: Significant savings in read-heavy workloads
3. Lifetime Elision in Return Types
#![allow(unused)] fn main() { // Explicit lifetimes fn process<'a>(input: &'a str) -> Cow<'a, str> { ... } // Lifetime elision (compiler infers) fn process(input: &str) -> Cow<str> { ... } }
- Lifetime elision rules: Compiler can infer lifetimes in many cases
- Input/output relationship: Lifetime of output tied to lifetime of input
- API simplification: Less verbose function signatures
- Borrow checker integration: Ensures borrowed data lives long enough
4. ToOwned Trait and Custom Implementations
#![allow(unused)] fn main() { pub trait ToOwned { type Owned: Borrow<Self>; fn to_owned(&self) -> Self::Owned; fn clone_into(&self, target: &mut Self::Owned) { ... } } }
- Generalized cloning: More flexible than
Clonefor borrowed/owned relationships - Associated type:
Ownedrepresents the owned version of borrowed data - Standard implementations:
str->String,[T]->Vec<T>,Path->PathBuf - Custom implementations: Define your own borrowed/owned type pairs
5. Zero-Copy String Processing
#![allow(unused)] fn main() { fn remove_prefix(input: &str, prefix: &str) -> Cow<str> { if let Some(stripped) = input.strip_prefix(prefix) { Cow::Borrowed(stripped) // Zero-copy slice of original string } else { Cow::Borrowed(input) // No modification needed } } }
- String slicing: Create views into existing strings without allocation
- Conditional processing: Only allocate when modification is actually needed
- Memory savings: Dramatic reduction in allocations for read-heavy operations
- Cache-friendly: Better memory locality when avoiding allocations
6. API Design for Flexible Input Types
#![allow(unused)] fn main() { // Accept multiple input types fn flexible_api(input: impl Into<Cow<str>>) -> String { let cow = input.into(); cow.into_owned() // Convert to owned String } // Usage examples: flexible_api("string literal"); // &str -> Cow::Borrowed flexible_api(String::from("owned")); // String -> Cow::Owned flexible_api(Cow::Borrowed("cow")); // Direct Cow }
Into<Cow<T>>pattern: Accept both borrowed and owned data seamlessly- Conversion traits: Automatic conversion from
&TandT::OwnedtoCow<T> - API flexibility: Single function signature handles multiple input types
- User convenience: Callers don't need to think about ownership
7. Memory Efficiency Patterns
#![allow(unused)] fn main() { // Efficient transformation pipeline fn transform_pipeline(input: &str) -> Cow<str> { let mut current = Cow::Borrowed(input); for transform in &transforms { current = transform.apply(current); // Chain without unnecessary cloning } current } }
- Pipeline efficiency: Avoid intermediate clones in transformation chains
- Lazy evaluation: Defer expensive operations until absolutely necessary
- Memory pressure reduction: Significant savings in allocation-heavy code
- Performance monitoring: Easy to track when allocations actually occur
These concepts demonstrate how Rust enables both memory efficiency and API flexibility through the type system. Cow is a powerful pattern for building performant code that doesn't sacrifice usability, allowing you to optimize for the common case (no modification needed) while still handling the general case (modification required) correctly.
Command Pattern
The Command pattern encapsulates a request as an object, allowing you to parameterize clients with different requests, queue operations, log requests, and support undo operations. In Rust, this pattern showcases the power of closures, function pointers, and different ways to capture and store computation.
Problem Statement
You need to decouple the object that invokes an operation from the object that performs it. You might want to queue operations, log them, support undo/redo, or pass operations as parameters. Traditional OOP uses command objects, but Rust's closures provide more flexible and efficient alternatives.
Traditional Object-Oriented Implementation
In Java, the Command pattern typically uses interfaces and classes:
// Command interface
interface Command {
void execute();
void undo();
}
// Receiver - the object that performs the actual work
class TextEditor {
private StringBuilder content = new StringBuilder();
public void insertText(String text) {
content.append(text);
}
public void deleteText(int length) {
int start = Math.max(0, content.length() - length);
content.delete(start, content.length());
}
public String getContent() {
return content.toString();
}
}
// Concrete commands
class InsertCommand implements Command {
private TextEditor editor;
private String text;
public InsertCommand(TextEditor editor, String text) {
this.editor = editor;
this.text = text;
}
@Override
public void execute() {
editor.insertText(text);
}
@Override
public void undo() {
editor.deleteText(text.length());
}
}
// Invoker
class Macro {
private List<Command> commands = new ArrayList<>();
public void addCommand(Command command) {
commands.add(command);
}
public void execute() {
for (Command command : commands) {
command.execute();
}
}
}
Rust Implementation
Rust provides multiple ways to implement the Command pattern, each with different trade-offs:
1. Function Pointers
Simple stateless commands using function pointers:
#![allow(unused)] fn main() { // Simple command using function pointers type SimpleCommand = fn(); struct Calculator { value: f64, } impl Calculator { fn new() -> Self { Calculator { value: 0.0 } } fn add(&mut self, n: f64) { self.value += n; println!("Added {}, result: {}", n, self.value); } fn multiply(&mut self, n: f64) { self.value *= n; println!("Multiplied by {}, result: {}", n, self.value); } fn clear(&mut self) { self.value = 0.0; println!("Cleared, result: {}", self.value); } } // Function that creates commands fn create_calculator_commands() -> Vec<fn(&mut Calculator)> { vec![ |calc| calc.add(10.0), |calc| calc.multiply(2.0), |calc| calc.add(5.0), |calc| calc.clear(), ] } fn demonstrate_function_pointers() { println!("=== Function Pointer Commands ==="); let mut calculator = Calculator::new(); let commands = create_calculator_commands(); for command in commands { command(&mut calculator); } } }
2. Closures for Stateful Commands
Closures can capture state, making them more flexible than function pointers:
#![allow(unused)] fn main() { use std::collections::VecDeque; #[derive(Debug, Clone)] struct TextEditor { content: String, } impl TextEditor { fn new() -> Self { TextEditor { content: String::new(), } } fn insert(&mut self, text: &str) { self.content.push_str(text); } fn delete(&mut self, count: usize) { let new_len = self.content.len().saturating_sub(count); self.content.truncate(new_len); } fn replace(&mut self, old: &str, new: &str) { self.content = self.content.replace(old, new); } fn get_content(&self) -> &str { &self.content } } // Command trait for undo/redo support trait Command { fn execute(&self, editor: &mut TextEditor); fn undo(&self, editor: &mut TextEditor); fn description(&self) -> &str; } // Concrete command implementations struct InsertCommand { text: String, } impl InsertCommand { fn new(text: String) -> Self { InsertCommand { text } } } impl Command for InsertCommand { fn execute(&self, editor: &mut TextEditor) { editor.insert(&self.text); } fn undo(&self, editor: &mut TextEditor) { editor.delete(self.text.len()); } fn description(&self) -> &str { "Insert text" } } struct DeleteCommand { count: usize, deleted_text: std::cell::RefCell<Option<String>>, } impl DeleteCommand { fn new(count: usize) -> Self { DeleteCommand { count, deleted_text: std::cell::RefCell::new(None), } } } impl Command for DeleteCommand { fn execute(&self, editor: &mut TextEditor) { let content = editor.get_content(); let start = content.len().saturating_sub(self.count); let deleted = content[start..].to_string(); *self.deleted_text.borrow_mut() = Some(deleted); editor.delete(self.count); } fn undo(&self, editor: &mut TextEditor) { if let Some(ref text) = *self.deleted_text.borrow() { editor.insert(text); } } fn description(&self) -> &str { "Delete text" } } // Command manager with undo/redo support struct CommandManager { history: VecDeque<Box<dyn Command>>, current_position: usize, max_history: usize, } impl CommandManager { fn new(max_history: usize) -> Self { CommandManager { history: VecDeque::new(), current_position: 0, max_history, } } fn execute(&mut self, command: Box<dyn Command>, editor: &mut TextEditor) { // Execute the command command.execute(editor); // Remove any commands after current position (for redo) self.history.truncate(self.current_position); // Add new command self.history.push_back(command); self.current_position = self.history.len(); // Maintain max history size while self.history.len() > self.max_history { self.history.pop_front(); self.current_position = self.current_position.saturating_sub(1); } } fn undo(&mut self, editor: &mut TextEditor) -> bool { if self.current_position > 0 { self.current_position -= 1; if let Some(command) = self.history.get(self.current_position) { command.undo(editor); return true; } } false } fn redo(&mut self, editor: &mut TextEditor) -> bool { if self.current_position < self.history.len() { if let Some(command) = self.history.get(self.current_position) { command.execute(editor); self.current_position += 1; return true; } } false } fn get_history(&self) -> Vec<String> { self.history.iter() .enumerate() .map(|(i, cmd)| { let marker = if i < self.current_position { "✓" } else { "-" }; format!("{} {}", marker, cmd.description()) }) .collect() } } fn demonstrate_undo_redo() { println!("\n=== Undo/Redo Commands ==="); let mut editor = TextEditor::new(); let mut manager = CommandManager::new(10); // Execute some commands manager.execute(Box::new(InsertCommand::new("Hello ".to_string())), &mut editor); println!("After insert: '{}'", editor.get_content()); manager.execute(Box::new(InsertCommand::new("World".to_string())), &mut editor); println!("After insert: '{}'", editor.get_content()); manager.execute(Box::new(DeleteCommand::new(5)), &mut editor); println!("After delete: '{}'", editor.get_content()); // Undo operations println!("\nUndoing..."); manager.undo(&mut editor); println!("After undo: '{}'", editor.get_content()); manager.undo(&mut editor); println!("After undo: '{}'", editor.get_content()); // Redo operations println!("\nRedoing..."); manager.redo(&mut editor); println!("After redo: '{}'", editor.get_content()); println!("\nCommand history:"); for (i, desc) in manager.get_history().iter().enumerate() { println!(" {}: {}", i + 1, desc); } } }
3. Boxed Closures for Dynamic Commands
For more flexibility, store closures in boxes:
#![allow(unused)] fn main() { type BoxedCommand<T> = Box<dyn Fn(&mut T) + Send + Sync>; struct EventSystem<T> { commands: Vec<BoxedCommand<T>>, } impl<T> EventSystem<T> { fn new() -> Self { EventSystem { commands: Vec::new(), } } fn add_command<F>(&mut self, command: F) where F: Fn(&mut T) + Send + Sync + 'static, { self.commands.push(Box::new(command)); } fn execute_all(&self, target: &mut T) { for command in &self.commands { command(target); } } fn clear(&mut self) { self.commands.clear(); } } #[derive(Debug)] struct GameState { score: u32, level: u32, player_name: String, } impl GameState { fn new(player_name: String) -> Self { GameState { score: 0, level: 1, player_name, } } fn add_score(&mut self, points: u32) { self.score += points; } fn level_up(&mut self) { self.level += 1; println!("Level up! Now at level {}", self.level); } fn reset(&mut self) { self.score = 0; self.level = 1; } } fn demonstrate_event_system() { println!("\n=== Event System with Boxed Closures ==="); let mut game = GameState::new("Alice".to_string()); let mut events = EventSystem::new(); // Add various event handlers events.add_command(|state: &mut GameState| { state.add_score(100); println!("Score bonus applied! Score: {}", state.score); }); events.add_command(|state: &mut GameState| { if state.score >= 500 { state.level_up(); } }); let bonus_multiplier = 2; events.add_command(move |state: &mut GameState| { let bonus = state.level * bonus_multiplier * 10; state.add_score(bonus); println!("Level bonus: {} points", bonus); }); println!("Initial state: {:?}", game); // Execute events multiple times for round in 1..=6 { println!("\nRound {}:", round); events.execute_all(&mut game); println!("State: {:?}", game); } } }
4. Advanced: Async Commands
Commands that perform asynchronous operations:
#![allow(unused)] fn main() { use std::future::Future; use std::pin::Pin; use std::time::Duration; type AsyncCommand<T> = Box<dyn Fn(&mut T) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>; struct AsyncEventSystem<T> { commands: Vec<AsyncCommand<T>>, } impl<T> AsyncEventSystem<T> where T: Send + 'static, { fn new() -> Self { AsyncEventSystem { commands: Vec::new(), } } fn add_command<F, Fut>(&mut self, command: F) where F: Fn(&mut T) -> Fut + Send + Sync + 'static, Fut: Future<Output = ()> + Send + 'static, { self.commands.push(Box::new(move |target| { Box::pin(command(target)) })); } async fn execute_all(&self, target: &mut T) { for command in &self.commands { command(target).await; } } } #[derive(Debug)] struct NetworkService { connected: bool, message_count: u32, } impl NetworkService { fn new() -> Self { NetworkService { connected: false, message_count: 0, } } async fn connect(&mut self) { println!("Connecting to network..."); tokio::time::sleep(Duration::from_millis(100)).await; self.connected = true; println!("Connected!"); } async fn send_message(&mut self, message: &str) { if self.connected { println!("Sending: {}", message); tokio::time::sleep(Duration::from_millis(50)).await; self.message_count += 1; println!("Message sent! Total: {}", self.message_count); } else { println!("Cannot send message: not connected"); } } async fn disconnect(&mut self) { if self.connected { println!("Disconnecting..."); tokio::time::sleep(Duration::from_millis(50)).await; self.connected = false; println!("Disconnected!"); } } } async fn demonstrate_async_commands() { println!("\n=== Async Commands ==="); let mut service = NetworkService::new(); let mut async_events = AsyncEventSystem::new(); // Add async commands async_events.add_command(|service: &mut NetworkService| async move { service.connect().await; }); async_events.add_command(|service: &mut NetworkService| async move { service.send_message("Hello, World!").await; }); async_events.add_command(|service: &mut NetworkService| async move { service.send_message("How are you?").await; }); async_events.add_command(|service: &mut NetworkService| async move { service.disconnect().await; }); println!("Initial state: {:?}", service); async_events.execute_all(&mut service).await; println!("Final state: {:?}", service); } }
Complete Example and Usage
// Note: This would need to be in an async context for the async example fn main() { println!("=== Command Pattern Examples ==="); // 1. Function pointer commands demonstrate_function_pointers(); // 2. Undo/redo with trait objects demonstrate_undo_redo(); // 3. Event system with boxed closures demonstrate_event_system(); // 4. Macro system demonstration demonstrate_macro_system(); // Note: async example would be run with: // tokio::runtime::Runtime::new().unwrap().block_on(demonstrate_async_commands()); } fn demonstrate_macro_system() { println!("\n=== Macro System ==="); #[derive(Debug, Default)] struct Document { content: String, saved: bool, } impl Document { fn write(&mut self, text: &str) { self.content.push_str(text); self.saved = false; } fn save(&mut self) { self.saved = true; println!("Document saved!"); } fn clear(&mut self) { self.content.clear(); self.saved = false; } } // Macro that records and can replay commands struct Macro { commands: Vec<Box<dyn Fn(&mut Document)>>, name: String, } impl Macro { fn new(name: String) -> Self { Macro { commands: Vec::new(), name, } } fn record<F>(&mut self, command: F) where F: Fn(&mut Document) + 'static, { self.commands.push(Box::new(command)); } fn execute(&self, document: &mut Document) { println!("Executing macro '{}':", self.name); for (i, command) in self.commands.iter().enumerate() { println!(" Step {}: Executing command", i + 1); command(document); } } fn command_count(&self) -> usize { self.commands.len() } } let mut doc = Document::default(); let mut greeting_macro = Macro::new("Greeting".to_string()); // Record commands in macro greeting_macro.record(|d| d.write("Hello, ")); greeting_macro.record(|d| d.write("World!")); greeting_macro.record(|d| d.write("\nWelcome to Rust!")); greeting_macro.record(|d| d.save()); println!("Document before macro: {:?}", doc); println!("Macro has {} commands", greeting_macro.command_count()); greeting_macro.execute(&mut doc); println!("Document after macro: {:?}", doc); // Execute macro again println!("\nExecuting macro again:"); greeting_macro.execute(&mut doc); println!("Document after second execution: {:?}", doc); } #[cfg(test)] mod tests { use super::*; #[test] fn test_command_manager_undo_redo() { let mut editor = TextEditor::new(); let mut manager = CommandManager::new(5); // Execute commands manager.execute(Box::new(InsertCommand::new("Hello".to_string())), &mut editor); manager.execute(Box::new(InsertCommand::new(" World".to_string())), &mut editor); assert_eq!(editor.get_content(), "Hello World"); // Undo assert!(manager.undo(&mut editor)); assert_eq!(editor.get_content(), "Hello"); assert!(manager.undo(&mut editor)); assert_eq!(editor.get_content(), ""); // Can't undo further assert!(!manager.undo(&mut editor)); // Redo assert!(manager.redo(&mut editor)); assert_eq!(editor.get_content(), "Hello"); assert!(manager.redo(&mut editor)); assert_eq!(editor.get_content(), "Hello World"); // Can't redo further assert!(!manager.redo(&mut editor)); } #[test] fn test_event_system() { let mut game = GameState::new("Test".to_string()); let mut events = EventSystem::new(); events.add_command(|state| state.add_score(100)); events.add_command(|state| state.level_up()); assert_eq!(game.score, 0); assert_eq!(game.level, 1); events.execute_all(&mut game); assert_eq!(game.score, 100); assert_eq!(game.level, 2); } #[test] fn test_macro_system() { let mut doc = Document::default(); let mut macro_cmd = Macro::new("Test".to_string()); macro_cmd.record(|d| d.write("Test")); macro_cmd.record(|d| d.save()); assert_eq!(doc.content, ""); assert!(!doc.saved); macro_cmd.execute(&mut doc); assert_eq!(doc.content, "Test"); assert!(doc.saved); } }
Performance Characteristics
| Implementation | Memory | Call Overhead | Flexibility |
|---|---|---|---|
| Function Pointers | Minimal | None | Low |
| Closures | Variable | Minimal | High |
| Trait Objects | Box allocation | Virtual dispatch | High |
| Async Commands | Future + Box | Async overhead | Very High |
Best Practices
- Use function pointers for simple, stateless commands
- Use closures when you need to capture state
- Use trait objects when you need undo/redo or complex command interfaces
- Consider async commands for I/O-bound operations
- Prefer move semantics to avoid lifetime issues in stored closures
- Use
Send + Syncbounds for thread-safe command systems
When to Use Each Approach
- Function pointers: Simple, stateless operations with maximum performance
- Closures: When you need to capture environment variables
- Trait objects: When you need complex command interfaces with undo/redo
- Boxed closures: When you need runtime flexibility and don't mind allocation
- Async commands: For I/O-bound or long-running operations
Core Rust concepts introduced
Building on all previous patterns, the Command pattern introduces the final set of advanced concepts related to function storage and execution:
1. Closure Traits: Fn, FnMut, and FnOnce
#![allow(unused)] fn main() { // FnOnce - can be called once, consumes captured variables let consume_closure: Box<dyn FnOnce()> = Box::new(move || { drop(owned_data); // Consumes owned_data }); // FnMut - can be called multiple times, mutates captured variables let mut counter = 0; let mut mutate_closure: Box<dyn FnMut()> = Box::new(|| { counter += 1; // Mutates captured variable }); // Fn - can be called multiple times, only borrows captured variables let data = vec![1, 2, 3]; let borrow_closure: Box<dyn Fn() -> usize> = Box::new(|| { data.len() // Only borrows data }); }
- Closure hierarchy:
FnimplementsFnMut,FnMutimplementsFnOnce - Capture semantics: Determines how closures interact with captured variables
- Call frequency:
FnOnce(once),FnMut(multiple, exclusive),Fn(multiple, shared) - Automatic inference: Rust chooses the most restrictive trait that works
2. Function Pointers (fn) vs Closures
#![allow(unused)] fn main() { // Function pointer - no captured environment fn regular_function(x: i32) -> i32 { x + 1 } let fn_ptr: fn(i32) -> i32 = regular_function; // Closure - can capture environment let captured_value = 10; let closure = |x: i32| x + captured_value; // Captures captured_value // Function pointers are a subset of Fn fn takes_fn<F: Fn(i32) -> i32>(f: F) -> i32 { f(5) } takes_fn(fn_ptr); // ✅ Function pointer implements Fn takes_fn(closure); // ✅ Closure also implements Fn }
- Function pointers: Zero-sized, no captured environment, can be cast to/from raw pointers
- Closures: May capture environment, implemented as anonymous structs
- Compatibility: Function pointers implement the
Fntrait - Performance: Function pointers have no indirection, closures may have captured data
3. Capture Modes: By Reference, By Value, By Move
#![allow(unused)] fn main() { let data = vec![1, 2, 3]; let immutable_ref = 42; let mut mutable_data = String::from("hello"); // Capture by reference (default for variables that can be borrowed) let by_ref = || { println!("Data length: {}", data.len()); // Borrows data println!("Immutable: {}", immutable_ref); // Borrows immutable_ref }; // Capture by mutable reference let by_mut_ref = || { mutable_data.push_str(" world"); // Mutably borrows mutable_data }; // Capture by value/move (explicit with 'move' keyword) let by_move = move || { println!("Owned data: {:?}", data); // Takes ownership of data println!("Owned ref: {}", immutable_ref); // Copies immutable_ref println!("Owned string: {}", mutable_data); // Takes ownership of mutable_data }; }
- Automatic inference: Rust chooses the minimal capture mode needed
movekeyword: Forces capturing by value/move- Copy vs Move:
Copytypes are copied, non-Copytypes are moved - Lifetime management:
moveclosures don't depend on captured variable lifetimes
4. move Keyword for Closure Ownership
#![allow(unused)] fn main() { fn create_command(message: String) -> Box<dyn Fn() + Send + 'static> { // Without 'move', this would try to borrow 'message' // But 'message' won't live long enough for the returned closure Box::new(move || { println!("Command: {}", message); // 'message' is moved into closure }) } // Usage: let cmd = create_command("Hello".to_string()); // 'message' is no longer accessible here, but closure owns it cmd(); // Works fine }
- Ownership transfer:
movetransfers ownership of captured variables to closure - Static lifetimes:
moveclosures can have'staticlifetime - Thread safety: Often required for closures sent across threads
- Independence:
moveclosures don't depend on original variable lifetimes
5. Boxed Closures for Dynamic Storage
#![allow(unused)] fn main() { // Store different types of closures in the same collection let mut commands: Vec<Box<dyn Fn(&mut GameState)>> = Vec::new(); commands.push(Box::new(|state| state.add_score(100))); commands.push(Box::new(move |state| { let bonus = captured_bonus; state.add_score(bonus); })); // Execute all commands for command in &commands { command(&mut game_state); } }
- Type erasure:
Box<dyn Fn(...)>erases the specific closure type - Dynamic dispatch: Runtime method resolution through vtable
- Collection storage: Store different closures in the same collection
- Heap allocation: Closures are stored on the heap
6. Higher-Order Functions and Combinators
#![allow(unused)] fn main() { // Function that returns a function fn create_multiplier(factor: i32) -> impl Fn(i32) -> i32 { move |x| x * factor } // Function that takes a function as parameter fn apply_operation<F>(operation: F, value: i32) -> i32 where F: Fn(i32) -> i32, { operation(value) } // Combinator pattern fn combine_operations<F, G>(f: F, g: G) -> impl Fn(i32) -> i32 where F: Fn(i32) -> i32, G: Fn(i32) -> i32, { move |x| g(f(x)) // Function composition } }
- Higher-order functions: Functions that operate on other functions
- Factory functions: Create specialized functions based on parameters
- Function composition: Combine multiple functions into new functions
impl Fnreturn type: Return opaque closure types without boxing
7. Callback Patterns and Event Systems
#![allow(unused)] fn main() { struct EventEmitter<T> { listeners: Vec<Box<dyn Fn(&T) + Send + Sync>>, } impl<T> EventEmitter<T> { fn on<F>(&mut self, callback: F) where F: Fn(&T) + Send + Sync + 'static, { self.listeners.push(Box::new(callback)); } fn emit(&self, event: &T) { for listener in &self.listeners { listener(event); } } } }
- Observer pattern: Register callbacks for events
- Thread safety:
Send + Syncbounds for multi-threaded use - Event propagation: Notify multiple listeners of state changes
- Decoupled communication: Loosely couple event producers and consumers
These concepts demonstrate Rust's powerful function system, from zero-cost function pointers to flexible closures with captured environments. The Command pattern showcases how Rust's ownership system and closure traits enable both performance and flexibility in storing and executing computation.