Skip to content

09 - Traits and Generics

What this session is

About an hour. You'll learn traits (Rust's interface system) and generics (type parameters). Together they give Rust most of what other languages get from inheritance, but more flexibly.

The problem traits solve

In object-oriented languages you'd say "Cat IS-A Animal, Dog IS-A Animal, both have a speak() method." Rust doesn't have inheritance. Instead, it has traits - shared behavior defined as a contract.

trait Animal {
    fn speak(&self) -> String;
}

struct Cat { name: String }
struct Dog { name: String }

impl Animal for Cat {
    fn speak(&self) -> String {
        format!("{} says meow", self.name)
    }
}

impl Animal for Dog {
    fn speak(&self) -> String {
        format!("{} says woof", self.name)
    }
}

fn main() {
    let c = Cat { name: String::from("Whiskers") };
    let d = Dog { name: String::from("Rex") };
    println!("{}", c.speak());
    println!("{}", d.speak());
}

What's new: - trait Animal { ... } declares the trait - a set of methods (with signatures, optionally with default bodies). - impl Animal for Cat { ... } provides the methods for Cat. Read as "implement Animal for Cat." - Any type with the Animal impl can be treated as an Animal.

You can have multiple traits for the same type (a struct can implement many traits). You can have one trait implemented for many types. This combinatorial freedom replaces inheritance.

Default method implementations

Traits can include default implementations:

trait Greeter {
    fn name(&self) -> String;

    fn greet(&self) -> String {
        format!("Hello, {}", self.name())
    }
}

greet has a default. Types implementing Greeter only need to provide name; they get greet for free (or can override it).

Functions taking traits

Two ways to write "a function that takes anything implementing trait X":

Way 1 - impl Trait:

fn announce(a: impl Animal) {
    println!("{}", a.speak());
}

Way 2 - generic parameter with trait bound:

fn announce<T: Animal>(a: T) {
    println!("{}", a.speak());
}

Both compile to the same thing. The impl Trait form is shorter; the generic form lets you reuse T if mentioned twice.

For taking references to anything implementing a trait:

fn announce(a: &impl Animal) {
    println!("{}", a.speak());
}

(Or use trait objects - see "dynamic dispatch" below.)

Generic functions

A function that works on any type:

fn largest<T: PartialOrd>(items: &[T]) -> &T {
    let mut largest = &items[0];
    for item in items {
        if item > largest {
            largest = item;
        }
    }
    largest
}

fn main() {
    let nums = vec![10, 25, 3, 99, 42];
    println!("{}", largest(&nums));        // 99

    let words = vec!["pear", "apple", "banana"];
    println!("{}", largest(&words));        // pear (alphabetical)
}

<T: PartialOrd> says: "T is any type that implements PartialOrd" (the trait for <, >, etc.). Inside the function, > works because we required it.

Without the bound, the compiler wouldn't let you use > on T - it doesn't know yet whether T can be compared.

Generic structs

You met Vec<T> and HashMap<K, V>. You can write your own:

struct Pair<A, B> {
    first: A,
    second: B,
}

impl<A, B> Pair<A, B> {
    fn new(a: A, b: B) -> Pair<A, B> {
        Pair { first: a, second: b }
    }
}

fn main() {
    let p: Pair<&str, i32> = Pair::new("alice", 30);
    println!("{}, {}", p.first, p.second);
}

The <A, B> after impl introduces the type parameters again - needed because the impl block must know what A and B are.

Common standard traits

You'll see these often:

Trait What it means
Debug type can be printed with {:?} (derivable via #[derive(Debug)])
Display type can be printed with {} (implement manually)
Clone type can be .clone()d
Copy type can be copied (primitives, etc.)
PartialEq / Eq type supports ==
PartialOrd / Ord type supports <, >
Hash type can be a HashMap key
Default type has a default value (T::default())
Iterator type can be iterated

The #[derive(...)] attribute auto-implements common traits for your types:

#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: f64,
    y: f64,
}

That gives you {:?} printing, .clone(), and == for free. Most structs derive Debug at minimum.

Trait objects: dyn Trait

When you want a heterogeneous collection (e.g., a Vec of different types that all implement the same trait), use a trait object:

trait Animal {
    fn speak(&self) -> String;
}

// ... Cat, Dog impls ...

fn main() {
    let animals: Vec<Box<dyn Animal>> = vec![
        Box::new(Cat { name: String::from("Whiskers") }),
        Box::new(Dog { name: String::from("Rex") }),
    ];
    for a in &animals {
        println!("{}", a.speak());
    }
}
  • Box<dyn Animal> - "a heap-allocated something-that-implements-Animal." The dyn keyword signals dynamic dispatch (the actual method gets resolved at runtime via a vtable, like Java/C++ virtual methods).
  • Box::new(...) puts the value on the heap.

The generic impl Trait form (above) uses static dispatch - the compiler generates a specialized version per concrete type. Faster but produces more code. dyn Trait uses dynamic dispatch - one version, with a small per-call cost.

Rule of thumb: prefer impl Trait when you can; use dyn Trait when you need a heterogeneous collection or to return a value whose type varies.

Lifetimes meet generics (preview)

Generic functions returning references need lifetime annotations:

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}

'a is a lifetime parameter. We met this in page 06. Most of the time the compiler infers them; occasionally you write them. Recognize the shape; learn deeply when you encounter a need.

Exercise

Replace src/main.rs:

  1. Define a trait Shape with one required method: fn area(&self) -> f64.

  2. Define structs Circle { radius: f64 }, Square { side: f64 }, Rectangle { width: f64, height: f64 }. #[derive(Debug)] on each.

  3. Implement Shape for each.

  4. Write a function fn total_area(shapes: &[Box<dyn Shape>]) -> f64 that sums all areas.

  5. In main, create a heterogeneous Vec<Box<dyn Shape>> with one of each, print each shape with {:?}, and print the total area.

Expected output (numbers may have more decimals):

Circle { radius: 2.0 } area=12.566370614359172
Square { side: 3.0 } area=9.0
Rectangle { width: 4.0, height: 5.0 } area=20.0
total: 41.57

Stretch: define a generic function fn max_by_area<T: Shape>(shapes: &[T]) -> &T that returns the shape with the largest area. Test with a Vec<Circle>.

What you might wonder

"Why traits instead of inheritance?" Inheritance forces a hierarchy and tight coupling. A Dog must commit to being-an-Animal at definition time. Traits let any type implement any set of traits in any order. More flexible; fewer accidental dependencies.

"What about default fields, multiple inheritance, etc.?" Rust doesn't have those. Composition (one struct having another as a field) plus traits is the answer.

"Why is Vec<Box<dyn Animal>> so wordy?" Each part has a purpose: Vec<...> is the list; Box<...> is "owned heap pointer" (needed because trait objects don't have known size); dyn Animal is the trait object. You'll get used to it; it's the cost of explicit memory and dispatch decisions.

"What's the cost of #[derive(Clone)] vs writing it?" None. Derive generates the same code you'd write. Use it.

Done

  • Define traits and implement them for types.
  • Use trait bounds on generic parameters.
  • Use impl Trait for function parameters.
  • Use Box<dyn Trait> for heterogeneous collections.
  • Derive Debug, Clone, PartialEq, etc. with #[derive(...)].

You've now covered the major language features. Next pages: testing, packaging, reading code, contributing.

Next: Tests →

Comments