11.4 Traits in Depth

Traits are a cornerstone of Rust’s type system, enabling polymorphism and shared behavior across diverse types. The following sections go deeper into trait objects, object safety, common standard library traits, constraints on implementing traits (the orphan rule), and associated types.

11.4.1 Trait Objects and Dynamic Dispatch

Rust provides dynamic dispatch through trait objects, in addition to the standard static dispatch:

fn draw_shape(shape: &dyn Drawable) {
    shape.draw();
}

A &dyn Drawable can refer to any type that implements Drawable.

trait Drawable {
    fn draw(&self);
}

struct Circle {
    radius: f64,
}

impl Drawable for Circle {
    fn draw(&self) {
        println!("Drawing a circle with radius {}", self.radius);
    }
}

fn main() {
    let circle = Circle { radius: 5.0 };
    draw_shape(&circle);
}

Although dynamic dispatch introduces a slight runtime cost (due to pointer indirection), it allows for more flexible polymorphic designs. We will revisit trait objects in detail in Chapter 20 when discussing object-oriented design patterns in Rust.

11.4.2 Object Safety

A trait is object-safe if it meets two criteria:

  1. All methods have a receiver of self, &self, or &mut self.
  2. No methods use generic type parameters in their signatures.

Any trait that fails these requirements cannot be converted into a trait object.

11.4.3 Common Traits in the Standard Library

Rust’s standard library includes many widely used traits:

  • Clone: For types that can produce a deep copy of themselves.
  • Copy: For types that can be duplicated with a simple bitwise copy.
  • Debug: For formatting using {:?}.
  • PartialEq and Eq: For equality checks.
  • PartialOrd and Ord: For ordering comparisons.

Most of these traits can be derived automatically using the #[derive(...)] attribute:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: f64,
    y: f64,
}
}

11.4.4 Implementing Traits for External Types

You may implement your own traits on types from other crates, but the orphan rule forbids implementing external traits on external types:

#![allow(unused)]
fn main() {
trait MyTrait {
    fn my_method(&self);
}

// Allowed: implementing our custom trait for the external type String
impl MyTrait for String {
    fn my_method(&self) {
        println!("My method on String");
    }
}
}
use std::fmt::Display;

// Not allowed: implementing an external trait on an external type
impl Display for Vec<u8> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:?}", self)
    }
}

11.4.5 Associated Types

Associated types let you define placeholder types within a trait, simplifying the trait’s usage. When a type implements the trait, it specifies what those placeholders refer to.

Why Use Associated Types?

They make code more succinct compared to using generics in scenarios where a trait needs exactly one type parameter. The Iterator trait is a classic example:

#![allow(unused)]
fn main() {
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}
}

Implementing a Trait with an Associated Type

#![allow(unused)]
fn main() {
struct Counter {
    count: usize,
}

impl Iterator for Counter {
    type Item = usize;

    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;
        if self.count <= 5 {
            Some(self.count)
        } else {
            None
        }
    }
}
}

Here, Counter declares type Item = usize, so next() returns Option<usize>.

Benefits of Associated Types

  • More Readable: Avoids repeated generic parameters when a trait is naturally tied to one placeholder type.
  • Stronger Inference: The compiler knows exactly what Item refers to for each implementation.
  • Clearer APIs: Ideal when a trait naturally has one central associated type.