GGistDev

Traits in Rust

Traits define shared behavior. Types implement traits to opt into that behavior. Use trait bounds to write generic code over behaviors.

Declaring and implementing traits

trait Area {
    fn area(&self) -> f64;                // required method
    fn describe(&self) -> String {        // default method
        String::from("shape")
    }
}

struct Circle { r: f64 }
impl Area for Circle {
    fn area(&self) -> f64 { std::f64::consts::PI * self.r * self.r }
}

Trait bounds on functions

fn total_area<T: Area>(shapes: &[T]) -> f64 {
    shapes.iter().map(|s| s.area()).sum()
}

// where-clause for readability
fn print_all<T>(xs: &[T])
where
    T: std::fmt::Display,
{
    for x in xs { println!("{x}"); }
}

Use impl Trait in argument/return position for simpler signatures:

fn first(xs: impl IntoIterator<Item = i32>) -> Option<i32> { xs.into_iter().next() }

Associated types

Prefer associated types for relationships inside traits.

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

Trait objects (dyn Trait)

Use &dyn Trait or Box<dyn Trait> for dynamic dispatch when you need heterogenous collections.

fn draw_all(xs: &[&dyn Area]) { for x in xs { println!("{}", x.describe()); } }

Object safety rules limit which traits can be made into trait objects (no generic methods, Self in wrong places).

Blanket impls and coherence

The orphan rule prevents conflicting impls: you can only implement a trait for a type if either the trait or the type is local to your crate.

Supertraits and auto traits

trait Printable: std::fmt::Display {}

Send/Sync are auto traits derived from field types.

Derive and standard traits

Derive common traits for convenience: Debug, Clone, Copy, Eq, PartialEq, Ord, Hash, Default.

Summary

  • Traits define behavior; impl them for your types
  • Use bounds/impl Trait to write generic code; associated types model type families
  • Use dyn Trait for dynamic dispatch; mind object-safety and coherence