Get Your Hands Wet with Traits Object of Rust

Reading Time: 5 minutes

Traits are the abstract mechanism for adding functionality to Types or it tells Rust compiler about functionality a type must provide.”
In this article, we will focus on Traits Objects in Rust Programming and how Dynamic Dispatch takes place.

Before delving into this article please refer to this Rust Traits: Quick Introduction to understand the basics of Traits. And go deep into Traits with Rust Traits: Deep Dive.

In this post we’ll go through with:

  • What are Trait Objects
  • How Trait Object Performs Dynamic Dispatch
  • Trait Object vs Traditional Object
  • Trait Objects vs Generic Type with Trait Bounds

What are Trait Objects

Trait Objects are normal values that store a value of any type that implements the given trait, where the precise type can only be known at run-time.
The concept of Generic with Trait Bounds Rust compiler won’t allow us to use multiple concrete types at the same time. But with the help of Trait Objects, we can work with multiple types to fill in for the trait object at run-time. In short, Trait Objects allows a heterogeneous set of types.

There are some ways through which we can define Trait Objects:

  • Box<T>
  • &
  • Arc
  • RC

These are the ways to perform the process of Dynamic Dispatch or to achieve the concept of Trait Objects.

How Trait Object Performs Dynamic Dispatch

Dynamic Dispatch is the term where the compiler can’t able to tell at compile time which is going to invoke, it figures out at run-time that which method to call or we can say its “late-binding“.

Trait Object performs Dynamic Dispatch by providing the concrete values at run-time. Trait Object consists of a “data” pointer and a “vtable” pointer. The data pointer addresses the data (of some unknown type) that the trait object is storing, and the vtable pointer points to the vtable(“virtual method table”) corresponding to the implementation of Trait for multiple types.

The methods of the trait can be called on a trait object via a special record of function pointers traditionally called a vtable (created and managed by the compiler). This operation can be seen as erasing the compiler’s knowledge about the specific type of the pointer, and hence trait objects are sometimes referred to as type erasure. So by this approach, the dynamic dispatch is performed in the Trait Object.

Trait Object vs Traditional Object

Traditional Object in other Programming Languages are like:

This the object of Object-Oriented, which contains both data and its Behavior.
For example: Like in Java you have a Class that has its data and all functionality called its Behavior. But in Rust, we both have in different entities like:

Like here, in Rust, we have data in the form of Enums or Structs and Behavior in the Traits or the implementation part.
So, to achieve the concept of Object-oriented in Functional Programming we have Trait Objects in the Rust Programming Language.

This is how we can achieve the concept of Object-Oriented in the Functional using Trait Objects. Here Rust uses the pointers inside the trait object to know which method to call.

Now let’s understand it with the help of an example:

pub trait Calculator {
    fn calculate(&self) -> i32;
}

pub struct Age {
    pub birth_year: i32,
    pub current_year: i32
}

pub struct Add {
    pub first_number: i32,
    pub second_number: i32
}

impl Calculator for Age {
    fn calculate(&self) -> i32 {
        print!("Age :");
        self.current_year - self.birth_year
    }
}
impl Calculator for Add {
    fn calculate(&self) -> i32 {
        print!("Addition :");
        self.first_number + self.second_number
    }
}

pub struct Computation {
    // Trait Object
    pub types: Vec<Box<dyn Calculator>>,
}

impl Computation {
    pub fn run(&self) {
        for component in self.types.iter() {
            println!("{}", component.calculate());
        }
    }
}

fn main() {
   let object = Computation{ types: vec![
        Box::new(Age{
            birth_year: 1996,
            current_year: 2019
        }),
        Box::new(Add{
            first_number: 10,
            second_number: 20
        })] };
    object.run();
}

This is the simple example to using Trait Object.

    // Trait Object
    pub types: Vec<Box<dyn Calculator>>,

This is how we can use Trait Object, we just wrap a Trait Calculator with a Box smart pointer and that Box into a vector so with the help of this we can have multiple concrete types that implement the Calculator Trait.

In the above example we have two different types of Age and Add struct, so from the main() we have passed both the types into a Trait Object. Here Rust compiler won’t know about how many different types a Trait Object can have or how large they are. It just uses the pointer to point the value because with the help of Box the value will store in the heap and always returns a pointer of same. By using Boxing we never get an error which tells “doesn’t have a size known at compile-time“.

Trait Objects vs Generic Type with Trait Bounds

A generic type parameter can only work with one concrete type at a time, whereas trait objects allow for multiple concrete types to fill in for the trait object at run-time.
Let’s Understand this line with the help of an example:

pub struct GenericVsObject<T: Calculator> {
    types: Vec<Box<T>>
}

impl<T> GenericVsObject<T>
    where T: Calculator {
    pub fn run(&self) {
        for data in self.types.iter() {
            println!("{}", data.calculate());
        }
    }
}

fn main() {
   let object = GenericVsObject{ types: vec![
    Box::new(Age{
        birth_year: 1996,
        current_year: 2019
    }),
    Box::new(Add{             ///    error
        first_number: 10,     ///    error
        second_number: 20     ///    error 
    })] };
}

Here we have a struct called GenericVsObject with a Trait Bound and has attribute types that contain the vector of Box<T>. And we have given the same functionality as we had in the Trait Object. But Generics doesn’t allow to use multiple concrete types at the same time. That’s why this program will not compile and will give error which is:

error[E0308]: mismatched types
  --> src/main.rs:20:14
   |
20 |       Box::new(Add{
   |  ______________^
21 | |       first_number: 10,
22 | |       second_number: 20
23 | |     })] };
   | |_____^ expected struct `traits::object::object::Age`, found struct `traits::object::object::Add`
   |
   = note: expected type `traits::object::object::Age`
              found type `traits::object::object::Add`

Thanks for reading!!!


Knoldus-blog-footer-image

Written by 

Pawan Singh Bisht is a Software Consultant at Knoldus Software LLP, having a strong experience of more than two years in the technology field. He has been well versed in the core implementation of Rust and Java. He loves to contribute to the community which he attained from the community.

Discover more from Knoldus Blogs

Subscribe now to keep reading and get access to the full archive.

Continue reading