All articles
Understanding Rust's Traits: An Introduction and Examples
Traits & Generics

Understanding Rust's Traits: An Introduction and Examples

What Are Traits?

By Luis SoaresMay 25, 2023Original on Medium

What Are Traits?

In Rust, a trait is a language feature that allows you to define abstract behaviours and methods that other types can implement, making it possible to abstract over behaviour. Traits define shared behaviour that different types can have in common.

To define a trait, we use the trait keyword, followed by the trait's name and a set of method signatures defined within curly braces {}.

Let's look at an example:

trait Speak {
    fn speak(&self);
}

In the example above, we've defined a trait Speak that has one method speak. Any type that implements this trait must define this method.

Implementing Traits

Once we've defined a trait, we can implement that trait for any data type. To do this, we use the impl keyword, followed by the trait name for the data type.

Let's implement our Speak trait for a Dog and a Human struct.

struct Dog {
    name: String,
}

struct Human {
    name: String,
}

impl Speak for Dog {
    fn speak(&self) {
        println!("{} says: Woof!", self.name);
    }
}

impl Speak for Human {
    fn speak(&self) {
        println!("{} says: Hello!", self.name);
    }
}

Here, we have defined two structures Dog and Human both of which have a name field. We then implemented the Speak trait for both structures with their versions of the speak method.

Using Traits

We can now make use of these traits in our functions. Here's an example:

fn make_speak<T: Speak>(t: T) {
    t.speak();
}

let dog = Dog { name: String::from("Fido") };
let human = Human { name: String::from("Alice") };

make_speak(dog);  // prints "Fido says: Woof!"
make_speak(human); // prints "Alice says: Hello!"

In the above code make_speak is a generic function that takes any type T that implements the Speak trait. We can now pass any type that implements Speak to this function.

Default Implementations

Rust also allows us to provide default implementations for methods in our trait. This means we can let types implementing our trait use the default method or override it with their own.

trait Speak {
    fn speak(&self) {
        println!("Hello, I can't specify my species yet!");
    }
}

impl Speak for Dog {
    // We don't provide a `speak` method here, so Dog uses the default.
}

impl Speak for Human {
    fn speak(&self) {
        println!("{} says: Hello, I am a human!", self.name);
    }
}

let dog = Dog { name: String::from("Fido") };
let human = Human { name: String::from("Alice") };

make_speak(dog);  // prints "Hello, I can't specify my species yet!"
make_speak(human); // prints "Alice says: Hello, I am a human!"

In this example, Dog uses the default speak method from the Speak trait, but Human provides its implementation.

Trait Bounds

Practice what you learned

Reinforce this article with hands-on coding exercises and AI-powered feedback.

View all exercises

Trait bounds can constrain the types used in a generic function. For instance, we can specify that the function parameter must implement a particular trait.

fn make_speak<T: Speak>(t: T) {
    t.speak();
}

In this function signature, T: Speak is a trait bound that means "any type T that implements the Speak trait."

Traits as Parameters

One of the most common uses of traits is in function and method parameters. They allow functions and methods to accept parameters of different types. If you have a function that takes a trait instead of a type, it can get any type that implements it. This is a fundamental way of achieving polymorphism in Rust.

Let's see how to use a trait as a parameter:

fn say_hello(speaker: &dyn Speak) {
    speaker.speak();
}

let dog = Dog { name: String::from("Fido") };
let human = Human { name: String::from("Alice") };

say_hello(&dog);  // prints "Fido says: Woof!"
say_hello(&human); // prints "Alice says: Hello!"

Here, say_hello accepts a reference to any type that implements the Speak trait.

Traits for Operator Overloading

Traits can also be used to overload certain operators for your types. Rust has unique traits in the standard library for overloading operators. For example, the std::ops::Add trait allows you to overload the + operator:

use std::ops::Add;

struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

let p1 = Point { x: 1, y: 0 };
let p2 = Point { x: 2, y: 3 };

let p3 = p1 + p2;

println!("Point: ({}, {})", p3.x, p3.y); // prints "Point: (3, 3)"

Here, we've implemented the Add trait for our Point struct, allowing us to use the + operator to add two Points together.

Traits and Inheritance

Rust doesn't have classical inheritance, unlike object-oriented languages, but you can define a trait in terms of another trait. This is a way of composing behaviours. This feature allows a trait to build upon another trait's functionality.

Here is an example:

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

trait Speak: Animal {
    fn speak(&self) {
        println!("{} can't speak", self.name());
    }
}

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

impl Speak for Dog {}

let dog = Dog { name: String::from("Fido") };
dog.speak();  // prints "Fido can't speak"

Here, we define a base trait Animal and another trait Speak that depends on Animal. An implementation of Speak hence requires the implementation of Animal.

Practice what you learned

Reinforce this article with hands-on coding exercises and AI-powered feedback.

View all exercises

Want to practice Rust hands-on?

Go beyond reading — solve interactive exercises with AI-powered code review on Rust Lab.