Using Trait objects in Rust
21st February 2016
creativcoder / 3min
This post aims to provide a very gentle introduction to traits objects in rust aimed at people who already are familiar with Traits in Rust. If you don't know about traits well, a good read is here. In brief, a trait is the specification of an interface. That interface can contain functions (both member, and non-member), types and constants. In Rust world, most of the constructs we use like operators, functions, loops are modelled as traits. Traits also serve the purpose of marking entities as being thread safe (the Send and Sync) or not safe. As an analogy to other programming language, traits are similar to typeclasses in haskell.
We get a trait object when things are referenced not by their actual type but by the trait that
they are implementing. Trait objects in Rust are denoted by an &
before their name. So when can we use a trait object? A possible use case is shown here. We will build our understanding by taking a real world example. Understanding this way eventually leads us to the insight of applying it into our day today rust code.
Let's say we want to create a musician object who is very versatile and our goal is to give him the ability, or more technically a method
by which he can play any instrument given to him.
Let's create the type:
struct Musician {
name:String
}
We'll then create a constructor for our Musician
so that it's easy to create a new Musician instance.
impl Musician {
fn new(name: &str) -> Self {
Musician { name: name.to_string() }
}
}
Let's also create some instruments for the musician to play.
A Piano.
struct Piano {
keys:usize
}
impl Piano {
fn new(key:usize) -> Self {
Piano { keys: key }
}
}
and a Guitar.
enum GuitarType {
Acoustic,
Electric
}
use GuitarType::*;
struct Guitar {
_type:GuitarType
}
impl Guitar {
fn new(_typ:GuitarType) -> Self {
Guitar { _type: _typ }
}
}
Now let's define a trait called Playable
, that has a method called play
.
trait Playable {
fn play(&self);
}
We'll impl the Playable
trait for both of our instruments:
impl Playable for Guitar {
fn play(&self) {
println!("playing guitar");
}
}
impl Playable for Piano {
fn play(&self) {
println!("playing piano");
}
}
Now comes the interesting part. Look closely.
impl Musician {
fn play_instrument(&self, ins:&Playable) {
ins.play();
}
}
fn main() {
let musician = Musician::new("Sam");
musician.play_instrument(&Piano::new(34));
musician.play_instrument(&Guitar::new(Acoustic));
}
In the code above, we are defining a generic method play_instrument
on our Musician
struct that can take any type (&Playable
) that implements the Playable trait. We have used a trait object here.
The play_instrument
method will take any type implementing the Playable
trait as a argument for ins
parameter, which in our case are Guitar
and Piano
.
Using trait objects, we have made our Musician
, play any instrument. In this way it play a guitar as well as a piano.
Trait objects mimics a sort of dynamic dispatch or runtime polymorphism.
It uses virtual method table internally, for resolving the appropriate method to call.
During invocation of the play_instrument
at runtime, Rust uses two pointers: one for the value with which the method was invoked and other which points to a table of methods for the given trait impls. After resolving the correct method using pointer arithmetic, it passes the value pointer to that specific method. This all happens at runtime. This means that trait objects have a runtime overhead as compared to static dispatch using only traits.
Hope that was helpful. Have a great day!
Want to share feedback, or discuss further ideas? Feel free to leave a comment here! Please follow Rust's code of conduct. This comment thread directly maps to a discussion on GitHub, so you can also comment there if you prefer.