Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Traits in Rust

Traits

Traits are Rust’s primary abstraction mechanism for polymorphism, code reuse, and defining shared behavior. A trait specifies, what functionality a type must provide, which methods exist, and optional default behaviors. Rust heavily favors composition through traits instead of inheritance.

#![allow(unused)]
fn main() {
// Defined with the trait keyword
// Any type implementing Vehicle promises it can
// perform the move_it() behavior.
trait Vehicle {
    // Only the signature
    fn move_it(&self) -> i32;
}

struct Car {
    // ...
}

// This is how we implement traits on a type
impl Vehicle for Car {
    fn move_it(&self) {
        // Logic to move car
    }
}
}
  • Examples from the standard library:
#![allow(unused)]
fn main() {
Clone
// Allows a value to be explicitly
// duplicated by creating a deep or custom copy.

Copy
// Allows a value to be duplicated
// implicitly through a simple bitwise copy.

Debug
// Enables a type to be printed in
// a programmer-friendly format using `{:?}`.

Display
// Enables a type to be printed in
// a user-friendly format using `{}`.

Iterator
// Defines how a type produces a
// sequence of values one at a time via `next()`.

IntoIterator
// Allows a type to be converted
// into an iterator so it can be used in a `for` loop.

PartialEq
// Enables equality comparisons (`==` and `!=`) between values.

Ord
// Enables total ordering comparisons (`<`, `>`, `<=`, `>=`)
// between values.

Read
// Provides methods for reading
// bytes from a data source such as a file or stream.

Write
// Provides methods for writing
// bytes to a destination such as a file or stream.
}
  • A type can implement many traits
#![allow(unused)]
fn main() {
impl Clone for Car { /* ... */ }
impl Debug for Car { /* ... */ }
impl Display for Car { /* ... */ }
}

Default Implementations

Traits can optionally provide default method implementations.

#![allow(unused)]
fn main() {
trait Vehicle {
    // Unlike this,
    fn honk(&self);

    // This has a default implementation
    fn move_it(&self) -> i32 {
        println!("I'm a vehicle and I'm moving!");
        return 0;
    }
}

struct Car {
    // ...
}
impl Vehicle for Car {
    fn honk(&self) {
        println!("HOOOONK!");
    }

    // Notice that we don't implement the `move_it` method
    // and relied on its default implementation.
}
}

Traits & Generics

Traits are commonly used with generic type parameters in method/function signatures to enforce all sorts of constraints. Here’s an example:

#![allow(unused)]
fn main() {
fn notify(item: &impl Summary) -> i32 {
    println!("{}", item.summarize());
    return 0;
}
// Or
fn notify<T: Summary>(item: &T) -> i32 {
    println!("{}", item.summarize());
    return 0;
}
// Or
fn notify<T>(item: &T) -> i32
where
    T: Summary,
{
    println!("{}", item.summarize());
    return 0;
}
}
  • Checkout the last section of my Generic Types markdown for more complex examples.

Implementing Methods Conditionally with Trait Bounds

Trait bounds are constraints that specify which traits a generic type must implement in order to be used. I’ve been using them in my examples all this time but didn’t call them out explicitly.

#![allow(unused)]
fn main() {
// This is a Trait Bound
<T: Display + Copy>

// This is also a trait bound
x: &(impl Display + Copy)
}

If you remember from my notes on generics, we implemented methods for generic types like this:

#![allow(unused)]
fn main() {
Point<T> {
    x: T,
    y: T,
}
impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}
// Or
impl<T, U> Point<T, U> {
    // Methods themselves can introduce additional generics.
    fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}
}

We could also implement methods for a generic type only when it had a specific concrete type like so:

#![allow(unused)]
fn main() {
impl Point<i32> {
    fn print_type(&self) {
        println!("My type is i32");
    }
}
}

We can combine generic impl blocks with trait bounds to conditionally implement methods that are only available when a generic type satisfies certain trait requirements.

#![allow(unused)]
fn main() {
impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("Largest is x = {}", self.x);
        } else {
            println!("Largest is y = {}", self.y);
        }
    }
}
}

The trait bound T: Display + PartialOrd ensures that cmp_display is only available for Pair<T> when T implements both Display and PartialOrd.