Rust Traits: Quick Introduction

Reading Time: 3 minutes

Traits are the abstract mechanism for adding functionality to types or it tells Rust compiler about functionality a type must provide. Traits are a way to group methods to define a set of behaviors necessary to accomplish some purpose. In a nutshell, Traits are kind of similar to interfaces in other languages with some differences.

Here, we focus on different aspects of Traits and I’ll try to demystify all the related knowledge.
In this blog, we’ll cover few scenarios like:

  • Trait Definition (Desired Behavior)
  • Trait Implementation
  • Default Implementation of Trait

Before understanding these scenarios, let’s understand the Traits first. Now as you all know Traits are similar to Interfaces of other languages, so Traits are used to provide a common functionality to any type.
After defining Trait any Type can implement it and provide custom bodies for Trait’s methods.

So lets try to understand the above scenarios:

Trait Definition (Desired Behavior)

Trait Definition is the initial step to understand the Trait‘s concept and Traits are defined by providing the name of the Trait followed by the trait keyword. While defining any Trait we have to provide method signatures(method declaration) inside the Trait.
Like:

pub trait Calculator {
    fn add(&self) -> i32;
    fn sub(&self) -> i32;
    fn div(&self) -> i32;
    fn mul(&self) -> i32;
}

In the above example, we have defined a Trait called Calculator and declare four methods [add(), sub(), div() and mul()] with &self as a parameter and set a return type to i32. So, whenever any Type will implement this Trait, will have to override all the four methods and will have to define their custom body.

Trait Implementation

In the previous section, we have defined a Trait with our desired behavior. Now in this section, we aim to get that desired behavior and define their custom bodies.
Trait Implementation is a kind of similar implementing an Interface in other languages, but differently, here we use impl keyword to implement a Trait then write Trait’s name which we want to implement and then use for keyword to define for whom we are going to implement a Trait. In the impl block, we define method bodies for all the methods declared in the Trait’s definition.
Like:

impl trait_name for type_name {
  ///
  method definitions
  /// 
}

The above piece of code shows the syntax to implement a Trait for a Type like a struct.
Let’s try to understand the implementation of Trait with the help of an example:

struct Data {
    first_num: i32,
    second_num: i32
}
impl Calculator for Data {
    fn add(&self) -> i32 {
        self.first_num + self.second_num
    }
    fn sub(&self) -> i32 {
        self.first_num - self.second_num
    }
    fn div(&self) -> i32 {
        self.first_num / self.second_num
    }
    fn mul(&self) -> i32 {
        self.first_num * self.second_num
    }
}
fn main() {
    println!("Output of Add: {}", Data {first_num:2, second_num: 2}.add());
    println!("Output of Sub: {}", Data {first_num:4, second_num: 2}.sub());
    println!("Output of Div: {}", Data {first_num:10, second_num: 2}.div());
    println!("Output of Mul: {}", Data {first_num:2, second_num: 2}.mul());
}

Output:

Here, we implement a Trait which we defined earlier and provide method bodies for all the methods.

Default Implementation of Trait

Rust allows you to provide a default implementation of the Trait’s methods.
Some important terms related to Default Implementation:

  • Overriding a method with default implementation is same as an overriding a method which doesn’t have default behavior.
  • A Type can keep the implementation or can override also.
  • In the same Trait, default implementation can call another method also.

Lets understand all the points with the help of example.

pub trait Calculator {
    fn add(&self) -> i32;
    fn sub(&self) -> i32;
    fn get_result(&self) {
        println!("The result of Addition is {}", self.add());
    }
}

struct Data {
    first_num: i32,
    second_num: i32
}

impl Calculator for Data {
    fn add(&self) -> i32 {
        self.first_num + self.second_num
    }

    fn sub(&self) -> i32 {
        self.first_num - self.second_num
    }
}

fn main() {
    Data {first_num:2, second_num: 2}.get_result();
    println!("Output of Sub: {}", Data {first_num:4, second_num: 2}.sub());
}

Output for this program is:

In the above example, get_result() method has default behavior and in its implementation, it calls the other method which doesn’t have default behavior. So, we don’t need to override that method which is defined by default, we only need to define those methods which don’t have their default implementation.


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.