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 associated Trait 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 than impl 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:

  1. See a make_it_talk(&person)
  2. lookup where Talk is on this Person struct
  3. make_it_talk(&self) call with mappings to Talk 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.