Traits

Traits are an important concept in Rust that allows you to specify "behaviors" for your data structures. A "behavior" can be an incredibly simple concept, like adding two numbers together. However, in large applications, such as web servers or games, a "behavior" might implement more complex logic.

The basic structure of a Rust trait looks like the following.

trait <TraitName> {
  // Function signatures
}

Traits can define zero or more function signatures as the shared behaviors. After defining a trait, the trait must also be implemented, for any types that you want to use it on.

Example: Animal Behavior

A variety of different animals can "run." Rather than implementing a different run() function on each type of Animal, a trait can define a common function that works for all animal types.

Let's start by definine two different animals as struct types.

#![allow(unused)]
fn main() {
struct Tiger {
  distance_ran: u32,
}

struct Rhino {
  distance_ran: u32,
}
}

Both of these animals can run, but does a tiger run at the same speed as a rhinoceros? Of course not! Even though these two types of animals share a behavior, the specific implementation of that behavior could be different.

Let's define a trait that allows both animal types to share a run() behavior. Our time input argument represents the number of seconds that the animal runs.

#![allow(unused)]
fn main() {
trait AnimalThatRuns {
  fn run(&mut self, time: u8);
}
}

After you define a trait, you also must implement the trait for each type. To accomplish this, we use the impl keyword. We specify the trait name that we want to implement, along with the type that we are implementing it for. The function implementation must match the signature of the function that the trait defines. That means your input arguments, output type, generic arguments, must match the trait's function definition.

For simplicity, let's say a Tiger runs at 60 feet per second, and a rhinoceros runs at 20 feet per second. We'll start by implementing the AnimalThatRuns trait for the Tiger type.

#![allow(unused)]
fn main() {
impl AnimalThatRuns for Tiger {
  fn run(&mut self, time: u8) {
    self.distance_ran += (time as u32)*60;
  }
}
}

Next, we'll implement the AnimalThatRuns trait for the Rhino type.

#![allow(unused)]
fn main() {
impl AnimalThatRuns for Rhino {
    fn run(&mut self, time: u8) {
      self.distance_ran += (time as u32)*20;
    }
}
}

Now that we have implemented the AnimalThatRuns trait for both the Tiger and Rhino types, we are guaranteed that we can use the run() function regardless of which object we have. Let's make both animals run for 5 seconds, and see what the result is.

fn main() {
  let mut t1 = Tiger{distance_ran: 0};
  let mut r1 = Rhino{distance_ran: 0};

  t1.run(5);
  r1.run(5);

  println!("Tiger ran distance of {} feet", t1.distance_ran);
  println!("Rhino ran distance of {} feet", r1.distance_ran);
}

You should see output like the following. The tiger ran 3x further than the rhinoceros, in the same amount of time!

Tiger ran distance of 300 feet
Rhino ran distance of 100 feet

Default Implementations

Traits can have a default implementation, which is used in the absence of a type-specific implementation. To specify a default implementation, simply include a function body in your trait definition.

#![allow(unused)]
fn main() {
trait AnimalThatRuns {
  fn run(&mut self, time: u8) {
    println!("Running for {} seconds", time);
  }
}
}

If you create a new type of animal, and implement AnimalThatRuns, without specifying a custom function definition for the run() function, then the default implementation will be used.

#![allow(unused)]
fn main() {
struct Lion {
  distance_ran: u32,
}

impl AnimalThatRuns for Lion { }
}

As you can see, we still must specify the impl block. Because there's a default implementation for the run() function in the trait block, we are not required to create a custom implementation.

fn main() {
  let mut my_lion = Lion{distance_ran: 0};
  my_lion.run(6);
}

If you run this program, you should see the following output.

Running for 6 seconds

Default Implementations Can't Access Struct Fields

Because the default function implementation doesn't know what type it could be implemented on, you will not be able to reference fields from the struct.

For example, if you try to set a value for the distance_ran field on the Lion type, in the default implementation, you'll receive an error similar to the following.

trait AnimalThatRuns {
  fn run(&mut self, time: u8) {
    println!("Running for {} seconds", time);
    self.distance_ran += 5;
  }
}

no field distance_ran on type &mut Self