Rust Traits & Generics â Polymorphism Without Inheritance
Rust traits and generics provide polymorphism through compile-time dispatch and zero-cost abstractions, letting you write reusable code without the runtime overhead of virtual method dispatch.
What You'll Learn
In this tutorial, you'll learn how Rust traits define shared behavior, how generics enable code reuse with type safety, how trait bounds constrain generic types, and how associated types and trait objects work.
Why It Matters
Traditional OOP inheritance creates deep hierarchies and fragile base class problems. Rust's trait system provides interface-like contracts without inheritance. Generics produce monomorphized code with no runtime cost. This combination gives you expressiveness with C-level performance.
Real-World Use
The standard library's <a href="/design-patterns/iterator/">Iterator</a> trait is implemented by every collection. Web frameworks use traits for request handlers. Embedded HAL (Hardware Abstraction Layer) uses traits to abstract over different microcontrollers. Durga Antivirus Pro uses traits to support multiple scan backends with zero dispatch overhead.
flowchart LR
G[Generic Code] --> M[Monomorphization]
M --> C1[Concrete Type A]
M --> C2[Concrete Type B]
M --> C3[Concrete Type C]
T[Trait Definition] --> I1[Impl for Type A]
T --> I2[Impl for Type B]
T --> I3[Impl for Type C]
C1 -->|&dyn Trait| DO[Dynamic Dispatch]
C2 --> DO
C3 --> DO
Prerequisites: Structs & Enums and Error Handling. Understanding Rust types is required.
Defining and Implementing Traits
A trait defines a set of methods that types can implement.
trait Encrypt {
fn encrypt(&self) -> Vec<u8>;
fn key_length(&self) -> usize;
}
struct SimpleCipher {
key: Vec<u8>,
data: Vec<u8>,
}
impl Encrypt for SimpleCipher {
fn encrypt(&self) -> Vec<u8> {
self.data.iter()
.enumerate()
.map(|(i, b)| b ^ self.key[i % self.key.len()])
.collect()
}
fn key_length(&self) -> usize {
self.key.len()
}
}
fn main() {
let cipher = SimpleCipher {
key: vec![0xAB, 0xCD],
data: vec![0x01, 0x02, 0x03, 0x04],
};
let encrypted = cipher.encrypt();
println!("Encrypted: {:02X?}", encrypted);
println!("Key length: {}", cipher.key_length());
}
Expected output:
Encrypted: [AA, CF, A8, CD]
Key length: 2
Generic Functions
Generic functions work with any type that satisfies the specified trait bounds.
use std::fmt::Display;
fn print_in_blocks<T: Display>(items: &[T], block_size: usize) {
for chunk in items.chunks(block_size) {
let parts: Vec<String> = chunk.iter().map(|i| i.to_string()).collect();
println!("[{}]", parts.join(", "));
}
}
fn main() {
print_in_blocks(&[1, 2, 3, 4, 5, 6], 2);
print_in_blocks(&["a", "b", "c", "d", "e"], 3);
}
Expected output:
[1, 2]
[3, 4]
[5, 6]
[a, b, c]
[d, e]
Trait Bounds with where
Complex trait bounds use the where clause for readability.
use std::fmt::Debug;
fn process_pipeline<T, U>(input: T) -> U
where
T: Debug + Clone,
U: Default + From<T>,
{
println!("Processing: {:?}", input);
let cloned = input.clone();
U::from(cloned)
}
#[derive(Debug, Clone)]
struct Input(String);
#[derive(Debug, Default)]
struct Output(String);
impl From<Input> for Output {
fn from(i: Input) -> Output {
Output(format!("Processed: {}", i.0))
}
}
fn main() {
let input = Input("hello".into());
let output: Output = process_pipeline(input);
println!("Output: {:?}", output);
}
Expected output:
Processing: Input("hello")
Output: Output("Processed: hello")
Associated Types
Associated types let traits define placeholder types that implementers specify.
trait Container {
type Item;
fn contains(&self, item: &Self::Item) -> bool;
fn len(&self) -> usize;
}
struct ByteBuffer {
data: Vec<u8>,
}
impl Container for ByteBuffer {
type Item = u8;
fn contains(&self, item: &u8) -> bool {
self.data.contains(item)
}
fn len(&self) -> usize {
self.data.len()
}
}
fn main() {
let buf = ByteBuffer { data: vec![1, 2, 3, 4, 5] };
println!("Contains 3: {}", buf.contains(&3));
println!("Contains 10: {}", buf.contains(&10));
println!("Length: {}", buf.len());
}
Expected output:
Contains 3: true
Contains 10: false
Length: 5
Trait Objects
Trait objects (dyn Trait) enable dynamic dispatch for runtime polymorphism.
trait Scanner {
fn scan(&self, data: &[u8]) -> Vec<String>;
}
struct SignatureScanner {
signatures: Vec<Vec<u8>>,
}
impl Scanner for SignatureScanner {
fn scan(&self, data: &[u8]) -> Vec<String> {
self.signatures.iter()
.filter(|sig| data.windows(sig.len()).any(|w| w == sig.as_slice()))
.map(|_| "Threat detected!".to_string())
.collect()
}
}
struct HeuristicScanner;
impl Scanner for HeuristicScanner {
fn scan(&self, data: &[u8]) -> Vec<String> {
let entropy = data.iter().map(|&b| b as f64).sum::<f64>() / data.len() as f64;
if entropy > 100.0 {
vec!["High entropy - possible encrypted payload".to_string()]
} else {
vec![]
}
}
}
fn run_scanner(scanner: &dyn Scanner, data: &[u8]) {
let results = scanner.scan(data);
for r in results {
println!(" {}", r);
}
}
fn main() {
let sig_scanner = SignatureScanner {
signatures: vec![vec![0x90, 0x90, 0x90]],
};
let heuristic = HeuristicScanner;
let data = vec![0x90, 0x90, 0x90, 0x00, 0x01];
println!("Signature scanner:");
run_scanner(&sig_scanner, &data);
println!("Heuristic scanner:");
run_scanner(&heuristic, &data);
}
Expected output:
Signature scanner:
Threat detected!
Heuristic scanner:
Common Mistakes
1. Using Trait Objects When Generics Suffice
Trait objects add dynamic dispatch overhead. Use generics (impl Trait or type parameters) when the concrete type is known at compile time.
2. Not Implementing Default Methods
Traits can provide default method implementations. Use them to reduce boilerplate in implementors.
3. Over-constraining Generic Parameters
Only specify the bounds you need. Extra bounds make functions less reusable.
4. Confusing impl Trait and dyn Trait
impl Trait is static dispatch (monomorphized). dyn Trait is dynamic dispatch (vtable). impl Trait is preferred for performance.
5. Orphan Rule Violations
You can implement a trait for a type only if either the trait or the type is local to your crate. Use the newtype pattern to work around this.
Practice Questions
1. What is a trait in Rust? A trait defines a set of methods that types can implement. It is similar to interfaces in other languages but supports default implementations and associated types.
2. What is the difference between generics and trait objects? Generics use static dispatch (monomorphization) producing separate code for each type. Trait objects use dynamic dispatch (vtables) with a single code path. Generics are faster, trait objects are more flexible.
3. What are associated types? Associated types are placeholder types defined in a trait that implementors specify. They allow traits to work with a single concrete type per implementation without making the trait generic.
4. What is the orphan rule? You can implement a trait for a type only if either the trait or the type is defined in your crate. This prevents conflicting implementations across crates.
5. Challenge: Design a StorageBackend trait with methods read, write, and delete. Implement it for LocalDisk and S3Storage using generic code.
Mini Project: Plugin System with Traits
trait Plugin {
fn name(&self) -> &str;
fn initialize(&mut self) -> Result<(), String>;
fn execute(&self, input: &str) -> String;
}
struct UppercasePlugin;
struct ReversePlugin;
struct LoggingPlugin {
log: Vec<String>,
}
impl Plugin for UppercasePlugin {
fn name(&self) -> &str { "Uppercase" }
fn initialize(&mut self) -> Result<(), String> { Ok(()) }
fn execute(&self, input: &str) -> String { input.to_uppercase() }
}
impl Plugin for ReversePlugin {
fn name(&self) -> &str { "Reverse" }
fn initialize(&mut self) -> Result<(), String> { Ok(()) }
fn execute(&self, input: &str) -> String {
input.chars().rev().collect()
}
}
impl Plugin for LoggingPlugin {
fn name(&self) -> &str { "Logger" }
fn initialize(&mut self) -> Result<(), String> { Ok(()) }
fn execute(&self, input: &str) -> String {
format!("[LOG: executed on {} chars]", input.len())
}
}
fn run_plugin_pipeline(plugins: &mut [Box<dyn Plugin>], input: &str) {
let mut result = input.to_string();
for plugin in plugins.iter_mut() {
if let Err(e) = plugin.initialize() {
println!("Plugin {} failed: {}", plugin.name(), e);
continue;
}
result = plugin.execute(&result);
println!("[{}]: {}", plugin.name(), result);
}
}
fn main() {
let mut plugins: Vec<Box<dyn Plugin>> = vec![
Box::new(UppercasePlugin),
Box::new(ReversePlugin),
Box::new(LoggingPlugin { log: vec![] }),
];
run_plugin_pipeline(&mut plugins, "Hello, World!");
}
FAQ
Related Concepts
What's Next
Explore Closures & Iterators for functional programming, and Design Patterns for idiomatic Rust code.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro