Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
194: Trait Objects and Dynamic Dispatch Revisited
We've talked about traits before, and we've touched on dyn. But if you're like me, the first time you tried to build a real-world system using trait objects, you probably hit a wall regarding "object safety." It's one of those Rust concepts that feels like a riddle until you actually break your code a few times.
To really get a handle on dynamic dispatch revisited, let's build a pluggable text processing pipeline. Imagine we're building a tool where a user can chain together different filters—like converting text to uppercase, trimming whitespace, or masking sensitive words—without the main program knowing exactly which filters are being used at compile time.
Sketching the Filter Interface
First, we need a trait that defines what a "filter" is. Since we want to store these filters in a collection and run them sequentially, we can't use generics for the collection itself (unless we only ever had one type of filter). We need dyn.
trait TextFilter {
fn apply(&self, input: &str) -> String;
}
Simple enough. Now let's create a couple of concrete implementations. I'll start with a basic uppercase filter and a trimming filter.
struct UppercaseFilter;
impl TextFilter for UppercaseFilter {
fn apply(&self, input: &str) -> String {
input.to_uppercase()
}
}
struct TrimFilter;
impl TextFilter for TrimFilter {
fn apply(&self, input: &str) -> String {
input.trim().to_string()
}
}
Hitting the Wall with Object Safety
Here is where I usually make a mistake when I'm moving too fast. I often think, "I might want to clone these filters later," so I'll try to add a clone method to the trait. Let's see what happens when I try to do this the "obvious" way:
trait TextFilter {
fn apply(&self, input: &str) -> String;
fn clone_filter(&self) -> Self; // I'm trying to return the concrete type here
}
The moment I try to put these into a Vec<Box<dyn TextFilter>>, the compiler will scream at me. It'll tell me that TextFilter cannot be made into an object because it has a method that returns Self.
Why? Because dyn TextFilter is a trait object. At runtime, Rust doesn't know if the concrete type is an UppercaseFilter (which might be 0 bytes) or some massive struct with a huge buffer. If a method returns Self, Rust has no idea how much space to allocate on the stack for that return value. This is the core of "object safety."
Making the Trait Object-Safe
To fix this, I have to stop returning Self. If I really need a way to duplicate the filter, I have to return a Box<dyn TextFilter> instead. This moves the return value to the heap, where the size is just a pointer, regardless of the underlying concrete type.
trait TextFilter {
fn apply(&self, input: &str) -> String;
fn clone_box(&self) -> Box<dyn TextFilter>;
}
impl TextFilter for UppercaseFilter {
fn apply(&self, input: &str) -> String {
input.to_uppercase()
}
fn clone_box(&self) -> Box<dyn TextFilter> {
Box::new(UppercaseFilter)
}
}
impl TextFilter for TrimFilter {
fn apply(&self, input: &str) -> String {
input.trim().to_string()
}
fn clone_box(&self) -> Box<dyn TextFilter> {
Box::new(TrimFilter)
}
}
Wiring the Pipeline Together
Now that our trait is object-safe, we can actually build our pipeline. We'll use a Vec of Box<dyn TextFilter>. This is the essence of dynamic dispatch: the Vec doesn't care about the concrete types; it just knows that whatever is in the box implements TextFilter.
struct Pipeline {
filters: Vec<Box<dyn TextFilter>>,
}
impl Pipeline {
fn new() -> Self {
Self { filters: Vec::new() }
}
fn add_filter(&mut self, filter: Box<dyn TextFilter>) {
self.filters.push(filter);
}
fn process(&self, text: &str) -> String {
let mut result = text.to_string();
for filter in &self.filters {
result = filter.apply(&result);
}
result
}
}
fn main() {
let mut my_pipeline = Pipeline::new();
my_pipeline.add_filter(Box::new(TrimFilter));
my_pipeline.add_filter(Box::new(UppercaseFilter));
let input = " hello rustaceans! ";
let output = my_pipeline.process(input);
println!("Result: '{}'", output); // "HELLO RUSTACEANS!"
}
Notice how process just iterates through the Vec. Every time it calls filter.apply(), Rust performs a vtable lookup to find the actual implementation for that specific object. It's a tiny bit slower than a static dispatch (generics), but it gives us the flexibility to add new filters at runtime without recompiling the Pipeline struct.
📋 Practical Task
Implementing a Redaction Filter for the Text Pipeline
Using the Pipeline and TextFilter architecture from the lesson, create a new concrete filter called RedactionFilter.
- The
RedactionFiltershould be initialized with aStringcontaining a word to be censored (e.g., "secret"). - The
applymethod should replace all occurrences of that word in the input text with"[REDACTED]". - Ensure you implement
clone_boxcorrectly so that theRedactionFilterremains object-safe. - In your
mainfunction, add theRedactionFilterto a pipeline alongside theUppercaseFilterand verify that it correctly masks the target word before or after the casing change.
There are no comments for now.