Open In App

Rust – Traits

Improve
Improve
Like Article
Like
Save
Share
Report

A trait tells the Rust compiler about functionality a particular type has and can share with other types. Traits are an abstract definition of shared behavior amongst different types. So, we can say that traits are to Rust what interfaces are to Java or abstract classes are to C++. A trait method is able to access other methods within that trait.

Trait Definition

Traits are defined by providing the name of the Trait followed by the keyword “trait”. While defining any Trait we have to provide method declaration inside the Trait.

Defining a trait:

pub trait Detail
{
    fn Description(&self) -> i32;
    fn years_since_launched(&self) -> i32;
  
}

In the above example, we have defined a Trait called Detail and declare two methods (Description(), years_since_launched()) with &self as a parameter and set a return type to i32. So, whenever any Type will implement this Trait, will have to override both methods and will have to define their custom body.

Trait Implementation: 

Trait implementation is similar to implementing an interface in other languages like java or c++. 

Here we use the keyword “impl” to implement a Trait then write Trait’s Name which we have to implement, and then we use the keyword “for” to define for whom we are going to implement a Trait. We will define method bodies for all the methods which have been declared in the Trait’s Definition inside the impl block.

Implementation of Trait:

impl trait_Name for type_Name {
  ///
  method definitions
  /// 
}

Let us understand the implementation of traits examples:

Example 1:

Let’s implement a built-in trait called Detail on a Car struct:

Rust




// Defining a Detail trait by defining the
// functionality it should include
pub trait Detail
{
    fn description(&self) -> String;
    fn years_since_launched(&self) -> i32;
   
}
 
struct Car
 {
  brand_name : String,
  color: String,
  launched_year : i32
}
 
// Implementing an in-built trait Detail
// on the Car struct
impl Detail for Car  {
 
 // Method returns an overview of the car
  fn description(&self) -> String{
    return format!("I have a {} which is {} in color.",
      self.brand_name, self.color);
  }
   
  // Method returns the number of years between
  // the launched year of this car i.e.
  // 2020 and the release year of the movie
   fn years_since_launched(&self) -> i32{
    return 2021 - self.launched_year;
  }
}
 
fn main()
{
  let car = Car{
  brand_name: "WagonR".to_string(),
  color: "Red".to_string(),
  launched_year:1992
  };
   
  let car2 = Car{
  brand_name: "Venue".to_string(),
  color: "White".to_string(),
  launched_year:1997
  };
   
  println!("{}", car.description());
  println!("The car was released {} years ago.\n",
    car.years_since_launched());
   
  println!("{}", car.description());
  println!("The car was released {} years ago.",
    car.years_since_launched());
}


Output:

I have a WagonR which is Red in color.
The car was released 29 years ago.

I have a WagonR which is Red in color.
The car was released 29 years ago.

Example 2:

Let’s implement a built-in trait called Maths on a Parameter struct:

Rust




// Defining a Maths trait by defining
// the functionality it should include
pub trait Maths
{
    fn area_of_rectangle(&self) -> i32;
    fn perimeter_of_rectangle(&self) -> i32;
}
 
struct Parameter
 {
  l:i32,
  b:i32
}
 
 
// Implementing an in-built trait Detail
// on the Parameter struct
impl Maths for Parameter  {
 
 // Method returns area of rectangle
  fn area_of_rectangle(&self) -> i32{
  return self.l*self.b ;
  }
   
  // Method returns the perimeter of rectangle
  fn perimeter_of_rectangle(&self) -> i32{
  return 2*(self.l+self.b);
    
  }
   
}
 
fn main()
{
  let para =Parameter{
  l: 5,
  b: 6
  };
   
  println!("The area of rectangle is {}.",
    para.area_of_rectangle());
  println!("The perimeter of the rectangle is {}.",
    para.perimeter_of_rectangle());
}


Output:

The area of rectangle is 30.
The perimeter of the rectangle is 22.

Drop Trait :

Drop trait is important to the smart pointer pattern. Drop trait lets us customize what happens when a value is about to go out of scope. Drop trait functionality is almost always used when implementing a smart pointer. For example, Box<T> customizes Drop to deallocate the space on the heap that the box points to.

Example:

Rust




struct SmartPointer {
 data: String,
}
 
// implementing Drop trait
impl Drop for SmartPointer {
 fn drop(&mut self) {
 println!("Dropping SmartPointer with data `{}`!", self.data);
 }
}
fn main() {
 let _c = SmartPointer { data: String::from("my stuff") };
 let _d = SmartPointer { data: String::from("other stuff") };
 println!("SmartPointers created.");
 }


In the above example, there is a SmartPointer struct whose custom functionality is to print Dropping SmartPointer when the instance goes out of scope.

Output:

SmartPointers created.
Dropping SmartPointer with data `other stuff`!
Dropping SmartPointer with data `my stuff`!

Iterator Trait:

The Iterator trait implements the iterators over collections such as arrays. Iterator trait relates each iterator type with the type of value it produces.

The trait requires only a method to be defined for the next element, which may be manually defined in an impl block (as in arrays and ranges), which returns one item of the iterator at a time( Option<Item>), next will return Some(Item) as long as there are elements and when the iteration is over, returns None to indicate that the iteration is finished.

Clone trait:

Clone trait is for types that can make copies of themselves. Clone is defined as follows: 

Rust




trait Clone: Sized {
fn clone(&self) -> Self;
fn clone_from(&mut self, source: &Self) {
*self = source.clone()
}
}


The clone method should construct an independent copy of self and return it. Since the method’s return type is self and functions may not return unsized values, the Clone trait itself extends the Sized trait (self types to be sized)

Rust does not automatically clone the values but it let you make an explicit method call. The reference-counted pointer types like Rc<T> and Arc<T> are exceptions: cloning one of these simply increments the reference count and hands you a new pointer.

Supertrait:

If we might need one trait to use another trait’s functionality. In this situation, we need to rely on the dependent trait which is also being implemented. Rust has a way to specify that a trait is an extension of another trait, giving us something similar to subclassing in other languages.

To create a subtrait, indicate that it implements the supertrait in the same way you would with a type: 

Rust




trait MyDebug : std::fmt::Debug {
    fn my_subtrait_function(&self);
}


Returning Traits with dyn: 

A trait object in Rust is similar to an object in Java or C++. A trait object is always passed by a pointer and has a vtable so that methods can be dispatched dynamically. VTable is a kind of function pointer array that contains the addresses of all virtual functions of this class.

Type of trait objects uses dyn Trait:

e.g; &dyn Bar or Box<dyn Bar> 

Function using trait objects:

fn f(b: &dyn Bar) -> usize

use std::fmt::Debug;

fn dyn_trait(n: u8) -> Box<dyn Debug> {
    todo!()
}

The dyn_trait function can return any number of types that implement the Debug trait and can even return a different type depending on the input argument.

Single variable, Argument, or Return value can take values of multiple different types. But Virtual dispatch tends to slower method calls. And Objects must be passed by pointer.

Example:

Rust




struct Breakfast{}
struct Dinner{}
 
trait Food {
    fn dish(&self) -> &'static str;
}
 
// Implement the `Food` trait for `breakfast`.
impl Food for Breakfast {
    fn dish(&self) -> &'static str {
        "bread-butter!"
    }
}
 
// Implement the `Food` trait for `dinner`.
impl Food for Dinner {
    fn dish(&self) -> &'static str {
        "paneer-butter-masala!"
    }
}
 
// Returns some struct that implements Food,
// but we don't know which one at compile time.
fn eat(n: i32) -> Box<dyn Food> {
    if n < 8 {
        Box::new(Breakfast {})
    } else {
        Box::new(Dinner {})
    }
}
 
fn main() {
    let n = 3;
    let food = eat(n);
    println!("You have chosen a random dish for today {}",
      food.dish());
}


Output: 

You have chosen a random dish for today bread-butter!

Concept of #[derive]:

#[derive] attribute can be used by the compiler to provide the basic implementation for some traits.if more complex behavior is required, traits can be implemented manually.

The following is a list of derivable traits:

Parameters Description
Comparison traits Eq, PartialEq, Ord, PartialOrd
Clone traits To create T from &T
copy traits To give a type ‘copy semantics’ 
Serialization Encodable, Decodable
Rand To create a random instance of a data type
Hash traits To compute a hash from &T
Zero To create a zero instance of a numeric data type
FromPrimitive To create an instance from a numeric primitive
Default traits To create an empty instance of a data type
Debug traits  To format a value using the {:?} formatter

Take a look at the below code:

Rust




#[derive(HelloWorld)]
struct Hello;
 
fn main() {
    Hello::hello_world();
}


The derive attribute allows new items to be automatically generated for data structures. It uses the MetaListPaths syntax to specify a list of traits to implement or paths to derive macros to process.

Operator overloading via traits:

Operator overloading is customizing the behavior of an operator in particular situations. We cannot create our own operator, but we can overload the operations and corresponding traits listed in std::ops by implementing the traits. 

Rust




use std::ops::Add;
struct Value1(u32);
struct Value2(u32);
impl Add<Value2> for Value1 {
 type Output = Value1;
 fn add(self, other: Value2) ->Value1 {
 Value1(self.0 + (other.0 * 1000))
 }
}




Last Updated : 20 Sep, 2021
Like Article
Save Article
Previous
Next
Share your thoughts in the comments
Similar Reads