Introduction
Rust is a powerful and expressive language. However its unique ownership model can be a challenge for experienced or novice programmer alike.
In this book we cover a variety of design patterns that can help solve many problems commonly encountered when writing Rust code.
Status
This book is in early development. As such, the structure and contents are subject to change.
If you spot any errors or omissions, please feel free to suggest an edit.
License
This book is released under the MIT License.
Common Patterns
New
Idea: Use an associated function as a type constructor.
#![allow(unused)] fn main() { struct MyType { value: Vec<u32> } impl MyType { /// Create a new [`MyType`]. fn new() -> Self { MyType { value: Vec::new() } } } // A new instance of `MyType` let my_class = MyType::new(); }
Newtype
Idea: Wrap an existing type in a struct to form a new type.
This allows you to use Rust's type checker to ensure that the correct type is used.
#![allow(unused)] fn main() { struct AccountId(u64); let account_id = AccountId(0x1234567812345678); }
Block expressions
Idea: Scope temporary variables in a block expression and return the result
#![allow(unused)] fn main() { use std::sync::{Arc, Mutex}; use std::thread; let counter = Arc::new(Mutex::new(0u32)); let t = { // Temporary variables let counter = Arc::clone(&counter); // Block return value thread::spawn(move || { *counter.lock().unwrap() += 1; }) }; t.join().unwrap(); }
Inner
Idea: Store a type's internal state in a private struct.
#![allow(unused)] fn main() { /// Public facing type. pub struct Public(Inner); /// Public API. impl Public { pub fn new() -> Self { Public(Inner { state: 0 }) } } /// Private inner state. struct Inner { state: u32 } /// Private API. impl Inner { fn increment(&mut self) { self.state += 1; } } }
Multithreading Patterns
Multithreading in Rust introduces some unique challenges. Unlike many other lanaguages, the compiler is strict on enforcing safe access to shared values.
Move
Idea: Move ownership of a value exclusively to a new thread.
#![allow(unused)] fn main() { use std::thread; let values = vec![1, 2, 3, 4, 5]; let t = thread::spawn(move || { // Thread takes exclusive ownership of `values` for value in values { println!("{}", value); } }); t.join(); }
Threadsafe
Idea: Make a type threadsafe by protecting the internal state with a Mutex
.
This type implements Clone
, so it can be easily shared between threads.
#![allow(unused)] fn main() { use std::sync::{Arc, Mutex}; #[derive(Clone)] struct Counter(Arc<Mutex<u64>>); impl Counter { fn new() -> Self { // Internal state is protected by a Mutex Counter(Arc::new(Mutex::new(0))) } fn value(&self) -> u64 { *self.0.lock().unwrap() } fn increment(&self) { *self.0.lock().unwrap() += 1; } } }
Handle
Idea: Split a class into an exclusive owned type and a thread-safe handle.
#![allow(unused)] fn main() { use std::io; use std::sync::mpsc; /// Worker with exclusive access to a resource. struct Worker { channel: mpsc::Receiver<Message> } impl Worker { /// Returns ([`Worker`], [`Handle`]) pair. fn new() -> (Self, Handle) { // Channel for sending messages to the worker. let (tx, rx) = mpsc::channel(); (Worker { channel: rx }, Handle { channel: tx }) } /// Function which takes exclusive access of [`Worker`]. fn run(&mut self) { loop { while let Ok(message) = self.channel.recv() { match message { Message::Foo(value) => println!("Foo: {}", value), } } } } } /// Handle for interfacing with the worker. #[derive(Clone)] struct Handle { channel: mpsc::Sender<Message> } impl Handle { fn foo(&self, value: i32) { self.channel.send(Message::Foo(value)).unwrap() } } /// Messages that can be sent to [`Worker`]. enum Message { Foo(i32) } }