Skip to content

Rust Traits & Generics — Polymorphism Without Inheritance

DodaTech Updated 2026-06-21 7 min read

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
â„šī¸ Info

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

What is the difference between impl Trait and dyn Trait?

impl Trait is static dispatch — the compiler generates specialized code for each concrete type. dyn Trait is dynamic dispatch — the concrete type is resolved at runtime through a vtable. Use impl Trait for performance, dyn Trait for runtime flexibility.

Can traits have fields?

No, traits define behavior (methods), not data. Use structs for data and traits for behavior. Default trait methods can access Self methods but not fields.

What is blanket implementation?

A blanket implementation implements a trait for all types that satisfy certain bounds. For example, impl<T: Display> ToString for T implements ToString for every type that implements Display.

Error Handling
Closures & Iterators
Design Patterns

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