Rust `dyn` Keyword
There are conventions in Rust that you see repeatedly, but it isn’t until your fifth (or tenth) time around that you have the context/focus/time to deep dive into the details.
One example of this is the concept of dyn
keyword:
“The
dyn
keyword is used to highlight that calls to methods on the associatedTrait
are dynamically dispatched. …Unlike generic parameters or impl Trait, the compiler does not know the concrete type that is being passed. …
…
dyn Trait
is likely to produce smaller code thanimpl Trait
/ generic parameters as the method won’t be duplicated for each concrete type.”
(Source: Rust Standard Library Documentation)
You can also read more about this concept in the trait object section of The Rust Programming Language book.
Coming from the object-oriented world in my past, and because Rust uses a trait (think interfaces) system to layer functionality to structs (think classes/objects). While there isn’t a 1:1 analog across the two, there are some overlaps in concepts that have helped me with understanding as I make the switch.
So with that, what options do we have?
Concrete Types of Structs
Using the concrete type, which is straightforward to read and write, we define the functionality of Talk
via a trait and attach it to the Person. It’s also possible in our impl
block to override what talking looks like.
// Functionality of talking..
trait Talk {
fn talk(&self) {
println!("Hello!");
}
}
// A person, who can talk.. (uses default talking)
struct Person {}
impl Talk for Person {}
// A function, for Person specifically to talk..
fn make_person_talk(person: &Person) {
person.talk();
}
// Tying it together..
fn main() {
let person = Person {};
make_person_talk(&person);
}
What if we want to expand this to have a Dog
and Cat
struct? We’d make those structs as well, implement Talk
– but we’d need to make separate functions for making them talk…
struct Cat {}
impl Talk for Cat { fn talk(&self) { println!("Meow!"); } }
struct Dog {}
impl Talk for Dog { fn talk(&self) { println!("Woof!"); } }
// One function for each type..
fn make_cat_talk(cat: &Cat) { cat.talk(); }
fn make_dog_talk(dog: &Dog) { dog.talk(); }
Still “simple,” but things are starting to get repeated, especially if we need to target tens or hundreds of different creature types.
What if we could just target Talk
?
So Just Use the Generic Type of Talk
, right?
But we’re in Rust: a compiled language. With that comes the issue of memory/function alignment, and ensuring that data in memory is at the expected locations/offsets. The Talk
trait a “concept” (like an interface, remember) but it isn’t implemented as a class/subclass like you might be used to.
// "&Talk" causes error: trait objects must include the `dyn` keyword
fn make_it_talk(creature: &Talk) {
creature.talk();
}
If this above approach was used, the compiler wouldn’t be able to guarantee where the function talk(&self)
is located relative to the struct being passed in because it could vary dramatically between types. It also doesn’t know the size of the type that you’re passing in, so additional logic would be needed to determine where to find things.
Knowing these issues, there’s a fancy way to support many things using a trait, with Dynamic Dispatch!
Dynamic Dispatch
In the previous section, the Rust compiler is not able to determine where the talk(&self)
function is located because the size/shape of the struct could be different between types.. and Talk
is a concept (a trait object
), but doesn’t have anything to directly point to– so we’ll need some kind of middleware to allow things to hook together.
Dynamic Dispatch is a mechanism that maps things you’re looking for on structs at runtime, using an internal “vtable” (virtual table) to look things up on-the-fly, like:
- See a
make_it_talk(&person)
- lookup where
Talk
is on thisPerson
struct make_it_talk(&self)
call with mappings toTalk
methods/data
With this flexibility, we can now write our code like…
// Use the `dyn` keyword here
fn make_it_talk(creature: &dyn Talk) {
creature.talk();
}
fn main() {
// ... Then use the same function throughout ...
make_it_talk(&person);
make_it_talk(&cat);
make_it_talk(&dog);
}
Tradeoffs
Having these two options puts the power into the programmer’s hands to determine what they want to do, but they do come with separate tradeoffs that may or may not be important based on the project’s size or complexity:
Concrete Types | Dynamic Dispatch | |
---|---|---|
Repetition | Each type of struct needs its own methods/signatures defined | Only needs to be implemented once, shared trait functionality |
Runtime Performance | Directly calls functions at expected location, and logic can be optimized for each type at compilation time | An additional step for populating the vtables and looking up the function location when accessing functions/data |
Binary Size | Each function will require its own implementation to be compiled, increasing size | A single implementation will be compiled, reducing size |
As for safety, both approaches will ensure the same type-safety we know and love from Rust, but it’s up to you to decide which is the right fit.