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