11.4 Traits in Depth

11.4.1 Trait Objects and Dynamic Dispatch

Traits can be used for polymorphism through trait objects, allowing for dynamic dispatch at runtime.

Trait Object Syntax:

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

Here, &dyn Drawable is a trait object representing any type that implements Drawable.

Example:

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);
}

Dynamic Dispatch: When you use trait objects, Rust uses dynamic dispatch to determine which method implementation to call at runtime. This introduces a slight runtime overhead but allows for flexible code.

Definition of Dynamic Dispatch: Dynamic dispatch is a process where the compiler generates code that will determine which method to call at runtime based on the actual type of the object. This is in contrast to static dispatch, where the method to call is determined at compile time.

11.4.2 Object Safety

Not all traits can be used to create trait objects. A trait is object-safe if it meets certain criteria:

  • All methods must have receivers (self, &self, or &mut self).
  • Methods cannot have generic type parameters.

Non-Object-Safe Trait Example:

trait NotObjectSafe {
    fn new<T>() -> Self;
}

You cannot create a trait object from NotObjectSafe because it has a generic method.

11.4.3 Common Traits in Rust

Rust's standard library provides many commonly used traits:

  • Clone: For types that can be cloned.
  • Copy: For types that can be copied bitwise.
  • Debug: For formatting types using {:?}.
  • PartialEq and Eq: For types that can be compared for equality.
  • PartialOrd and Ord: For types that can be compared for ordering.

Deriving Traits:

You can automatically implement some traits using the #[derive] attribute.

Example:

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

11.4.4 Implementing Traits for External Types

You can implement your own traits for external types, but you cannot implement external traits for external types. This is known as the orphan rule.

Allowed:

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

impl MyTrait for String {
    fn my_method(&self) {
        println!("My method on String");
    }
}
}

Not Allowed:

use std::fmt::Display;

// Cannot implement external trait for external type
impl Display for Vec<u8> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // Implementation...
        write!(f, "{:?}", self)
    }
}

11.4.5 Associated Types

Traits can have associated types, which allow you to specify placeholder types that are determined by the implementer.

Example:

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

Here, Item is an associated type that will be specified by the implementing type.

Implementing with Associated Types:

#![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
        }
    }
}
}