Phase 4Code organization

#14 Traits

trait, impl, dyn

What is a trait?

A trait defines shared behavior. It is similar to interfaces in other languages: it declares a set of methods that types can implement. Traits enable polymorphism in Rust.

The standard library is full of traits: Display, Debug, Clone, Iterator, and many more.

Defining traits

You define a trait with the trait keyword and list the method signatures:

Defining a trait
trait Animal {
    fn name(&self) -> &str;
    fn sound(&self) -> &str;
}

A trait can require multiple methods. Each method signature ends with a semicolon -- no body is provided here, just the contract.

Implementing traits

You implement a trait for a type with impl TraitName for Type:

Implementing a trait
struct Dog {
    name: String,
}

struct Cat {
    name: String,
}

impl Animal for Dog {
    fn name(&self) -> &str {
        &self.name
    }

    fn sound(&self) -> &str {
        "Woof!"
    }
}

impl Animal for Cat {
    fn name(&self) -> &str {
        &self.name
    }

    fn sound(&self) -> &str {
        "Meow!"
    }
}

fn main() {
    let dog = Dog { name: String::from("Rex") };
    let cat = Cat { name: String::from("Whiskers") };

    println!("{} says: {}", dog.name(), dog.sound());
    println!("{} says: {}", cat.name(), cat.sound());
}

Default methods

Traits can provide default implementations. Types can override them or use the defaults:

Default methods
trait Describable {
    fn name(&self) -> &str;

    // Default implementation
    fn description(&self) -> String {
        format!("An object called {}", self.name())
    }
}

struct Dog {
    name: String,
}

impl Describable for Dog {
    fn name(&self) -> &str {
        &self.name
    }

    // Override the default
    fn description(&self) -> String {
        format!("{}: A loyal companion", self.name())
    }
}

struct Cat {
    name: String,
}

impl Describable for Cat {
    fn name(&self) -> &str {
        &self.name
    }

    fn description(&self) -> String {
        format!("{}: An independent feline", self.name())
    }
}

fn main() {
    let dog = Dog { name: String::from("Dog") };
    let cat = Cat { name: String::from("Cat") };
    println!("{}", dog.description());
    println!("{}", cat.description());
}

Trait bounds

You can use traits as constraints on generic functions. This is called a trait bound -- it ensures the type has the required behavior:

Trait bounds
trait Shape {
    fn area(&self) -> f64;
}

struct Circle {
    radius: f64,
}

struct Square {
    side: f64,
}

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

impl Shape for Square {
    fn area(&self) -> f64 {
        self.side * self.side
    }
}

// Trait bound: T must implement Shape
fn print_area<T: Shape>(shape: &T) {
    println!("Area: {:.2}", shape.area());
}

// Alternative syntax with impl Trait
fn print_area_alt(shape: &impl Shape) {
    println!("Area: {:.2}", shape.area());
}

fn main() {
    let circle = Circle { radius: 5.0 };
    let square = Square { side: 5.0 };

    print_area(&circle); // Area: 78.54
    print_area(&square); // Area: 25.00
}

You can also use dyn Trait for dynamic dispatch with trait objects:

Trait objects
fn describe(shape: &dyn Shape) {
    println!("This shape has area: {:.2}", shape.area());
}

fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![
        Box::new(Circle { radius: 3.0 }),
        Box::new(Square { side: 4.0 }),
    ];

    for shape in &shapes {
        describe(shape.as_ref());
    }
}

Summary

Quick reference
trait Name {                    // Define a trait
    fn method(&self) -> Type;    // Required method
    fn default(&self) { ... }    // Default method
}

impl Name for MyStruct { ... }  // Implement trait

fn foo<T: Trait>(x: &T)         // Trait bound
fn foo(x: &impl Trait)          // Shorthand
fn foo(x: &dyn Trait)           // Dynamic dispatch

Box<dyn Trait>                  // Trait object on heap

Your turn

Try running the traits demo with cargo run and check for errors with cargo check:

terminal — cargo
user@stemlegacy:~/traits-demo$