Rust Traits (Interfaces) Tutorial
In this Rust tutorial we learn how to use Traits (interfaces) to build more compact, loosely coupled applications.
We learn how to define traits and their abstract or concrete methods, how to implement different logic for each in their implementation, how to use implemented traits and how to implement multiple traits to a single struct.
What is a trait
A trait is, essentially, a struct that doesn’t define bodies for its methods. We only declare what the method should look like, not its logic. The benefit is that we can force other developers to follow a standard set of behaviours.
If you’re familiar with an object-oriented language like C#, a trait would be similar to an interface .
Interfaces help developers build applications that favor a Has-A relationship instead of an Is-A relationship and are as loosely coupled as possible.
Unlike interfaces however, traits can contain concrete methods (methods with a body) or abstract methods (methods without a body).
How to define a trait
To define a trait, we use the keyword trait , followed by a name and a body that may contain either abstract methods, concrete methods, or both.
trait TraitName {
// abstract (empty) method
fn method_name(&self);
// concrete (regular) method
fn method_name(&self) {
// concrete method body
}
}
Trait methods must have the &self parameter as the first parameter for the method. Any extra parameters we want to specify must come after it.
fn main() {}
// define a trait
trait Flyable {
fn flying(&self);
}
In the example above, we create a trait Flyable with a single abstract method.
Any entity that can fly will be able to implement this trait and its method. But, because two entities won’t necessarily fly the same way (sparrow vs a plane), the logic for the actual flying will be done by each separately.
How to implement a trait
We implement a trait almost the same way as a struct method . But, in the case of a trait, we specify for which trait we’re implementing with the for keyword.
// struct
struct StructName {}
// define trait
trait TraitName {
fn method_name(&self);
}
// implement trait for struct
impl TraitName for StructName {
fn method_name(&self) {
// the method logic
// specific to StructName
}
}
So, essentially what we’re doing is to define a body for the method for a particular struct. An example will make it clearer.
fn main() {}
// structs
struct Sparrow {}
struct Plane {}
// trait
trait Flyable {
fn flying(&self);
}
// implement trait for structs
impl Flyable for Sparrow {
fn flying(&self) {
println!("Sparrow flying at 10 speed");
}
}
impl Flyable for Plane {
fn flying(&self) {
println!("Plane flying flying at 100 speed");
}
}
Both a sparrow and a plane can fly, so we define the Flyable trait which can be used by both the sparrow and the plane. But, because they fly at dramatically different speeds, the logic in their methods can’t be the same.
Instead of having to write two different methods for each struct, we can simply define an abstract method in the trait and allow the implementations to handle the logic. When we then implement the trait for each of the structs, we can apply different logic to them.
How to use (call) an implemented trait method
Now that we’ve implemented the logic for the trait method, we can simply call the method as we would normally.
fn main() {
let sparrow = Sparrow {};
let plane = Plane {};
sparrow.flying();
plane.flying();
}
// structs
struct Sparrow {}
struct Plane {}
// trait
trait Flyable {
fn flying(&self);
}
// implement trait for structs
impl Flyable for Sparrow {
fn flying(&self) {
println!("Sparrow flying at 10 speed");
}
}
impl Flyable for Plane {
fn flying(&self) {
println!("Plane flying at 100 speed");
}
}
This seems like a lot of writing when we only have a single method. But, when more than one method is introduced, the benefit becomes clear.
We would most likely need to add more methods to the Flyable trait, like take_off, landing, etc. If we had to write individual methods for both structs, the code would quickly become unnecessarily bloated.
We also force developers to use the same standard set of behaviors for all entities that can fly, even if they do so differently.
How to implement multiple traits
We can implement more than one trait for each struct. To do this we simple add another trait, and implement it for the struct we want.
fn main() {
let sparrow = Sparrow {};
let plane = Plane {};
sparrow.flying();
sparrow.hopping();
plane.flying();
}
// structs
struct Sparrow {}
struct Plane {}
// trait
trait Flyable {
fn flying(&self);
}
trait Hoppable {
fn hopping(&self);
}
// implement traits for sparrow
impl Flyable for Sparrow {
fn flying(&self) {
println!("Sparrow flying");
}
}
impl Hoppable for Sparrow {
fn hopping(&self) {
println!("Sparrow hopping");
}
}
// implement trait for plane
impl Flyable for Plane {
fn flying(&self) {
println!("Plane flying");
}
}
In the example above, we add another trait called Hoppable with a hopping() method. Obviously the plain can’t hop, but the sparrow can so we implement it only for the sparrow.
If we added a penguin, we could implement Hoppable, but not Flyable. Adding a fish on the other hand would mean we need another trait called Swimmable, and then the penguin could also implement Swimmable.
This allows our applications to be as loosely coupled and compact as possible. We absolutely love traits (and interfaces in other languages) and recommend using them as much as poosible for bigger and more complex projects.
Summary: Points to remember
- Methods in a trait can be both abstract (without a body), or concrete (with a body).
- A trait can have more than one method associated inside of it.
- A trait is implemented for a specific struct similar to a function.
- If a trait contains an abstract method that we want to implement, we must define its body in the implementation.
- We can implement multiple traits for a single struct by simple adding and implementing them.
- Trait methods are called normally, nothing special is needed to use them, except an implementation.