Industrial Training




Rust Traits

    Rust trait is a feature of a Rust language that describes the functionality of each type that it can provide.
    A trait is similar to the feature of an interface defined in other languages.
    A trait is a way to group the method signatures to define a set of behaviors.
    A Trait is defined by using the trait keyword.

The Syntax of the trait:
trait trait_name  
  
    //body of the trait.  

In the above case, we declare the trait followed by the trait name. Inside the curly brackets, method signature is declared to describe the behavior of a type that implements the trait.


Let's see a simple example:
 struct Triangle  
{  
  base : f64,  
  height : f64,  
}  
trait HasArea  
{  
  fn area(&self)->f64;  
}  
  
impl HasArea for Triangle  
{  
  fn area(&self)->f64  
  {  
    0.5*(self.base*self.height)  
  }  
}  
fn main()  
{  
  let a = Triangle{base:10.5,height:17.4};  
  let triangle_area = a.area();  
  println!("Area of a triangle is {}",triangle_area);   
}  

Output:
Area of a triangle is 91.35

In the above example, trait named as HasArea is declared which contains the declaration of area() function. HasArea is implemented on the type Triangle. An area() function is simply called by using the instance of the structure, i.e., a.area().


Trait as Arguments


Traits can also be used as arguments of many different types.
The above example implements the HasArea trait, and it contains the definition of the area() function. We can define the calculate_area() function that calls the area() function, and the area() function is called using the instance of the type that implements the HasArea trait.


Let's look at the syntax:
fn calculate_area(item : impl HasArea)  
  
  println!("Area of the triangle is : {}",item.area());  
}   

Trait bounds on Generic functions


Traits are useful because they describe the behavior of different methods. But, Generic functions does not follow this constraint. Let's understand this through a simple scenario:


fn calculate_area( item : T)  
  
   println!(?Area of a triangle is {}?, item.area());  

In the above case, Rust compiler throws an "error that no method named found of type T". If we bound the trait to the generic T, then the following error can be overcome:


 fn calculate_area< T : HasArea> (item : T)  
{  
println!("Area of a triangle is {} ",item.area());  
  
  
}  

In the above case, < T: HasArea> means "T can be of any type that implements HasArea trait". Rust compiler got to know that any type that implements the HasArea trait will have an area() function.


Let's see a simple example:
 trait HasArea  
{  
  fn area(&self)->f64;  
}  
struct Triangle  
{  
  base : f64,  
  height : f64,  
}  
  
impl HasArea for Triangle  
{  
  fn area(&self)->f64  
  {  
    0.5*(self.base*self.height)  
  }  
}  
struct Square  
{  
  side : f64,  
}  
  
impl HasArea for Square  
{  
  fn area(&self)->f64  
  {  
     self.side*self.side  
  }  
}  
fn calculate_area< T : HasArea>(item : T)  
{  
  println!("Area is : {}",item.area());  
}  
  
fn main()  
{  
  let a = Triangle{base:10.5,height:17.4};  
  let b = Square{side : 4.5};  
  calculate_area(a);  
  calculate_area(b);  
}  

Output:
Area is : 91.35
Area is : 20.25

In the above example, calculate_area() function is generic over "T".


Rules for implementing traits


There are two limitations to implementing the trait:


    If the trait is not defined in your scope, then it cannot be implemented on any data type.

Let's see a simple example:
 use::std::fs::File;  
fn main()  
{  
  let mut f = File::create("hello.txt");  
  let str = "javaTpoint";  
  let result = f.write(str);  
}  

Output:
error : no method named 'write' found.
           let result = f.write(str);

In the above case, Rust compiler throws an error, i.e., "no method named 'write' found" as use::std::fs::File; namespace does not contain the write() method. Therefore, we need to use the Write trait to remove the compilation error.


    The trait which we are implementing must be defined by us. For example: If we define the HasArea trait, then we can implement this trait for the type i32. However, we could not implement the toString trait defined by the Rust for the type i32 as both the type and trait are not defined in our crate.

Multiple trait bounds


    Using '+' operator.

If we want to bound the multiple traits, we use the + operator.


Let's see a simple example:
 use std::fmt::{Debug, Display};  
fn compare_prints< T: Debug + Display>(t: &T)  
 {  
 println!("Debug: '{:?}'", t);  
 println!("Display: '{}'", t);  
}  
  
  
  
  
  
fn main() {  
    let string = "javaTpoint";  
    compare_prints(&string);  
    }  

Output:
Debug: ' "javaTpoint"'
Display: ' javaTpoint'

In the above example, Display and Debug traits are bounded to the type 'T' by using the '+' operator.


Using 'where' clause.
    A bound can be written using a 'where' clause which appears just before the opening bracket '{'.
    A 'where' clause can also be applied to the arbitrary types.
    When 'where' clause is used, then it makes the syntax more expressive than the normal syntax.

Let's look:
fn fun< T: Display+Debug, V: Clone+Debug>(t:T,v:V)->i32  
  
    //block of code;  

When 'where' is used in the above case:


fn fun< T, V>(t:T, v:V)->i32  
  where T : Display+ Debug,   
             V : Clone+ Debug  
  
       //block of code;  

In the above cases, the second case where we have used the 'where' clause makes the program more expressive and readable.


Let's see a simple example:
 trait Perimeter  
{  
  fn a(&self)->f64;  
}  
struct Square  
{  
  side : f64,  
}  
impl Perimeter for Square  
{  
  fn a(&self)->f64  
  {  
    4.0*self.side  
  }  
}  
struct Rectangle  
{  
 length : f64,  
 breadth : f64,  
}  
impl Perimeter for Rectangle  
  
{  
 fn a(&self)->f64  
 {  
   2.0*(self.length+self.breadth)  
 }  
}  
fn print_perimeter< Square,Rectangle>(s:Square,r:Rectangle)  
  where Square : Perimeter,  
        Rectangle : Perimeter  
        {  
          let r1 = s.a();  
          let r2 = r.a();  
          println!("Perimeter of a square is {}",r1);  
          println!("Perimeter of a rectangle is {}",r2);  
        }  
        fn main()  
        {  
          let sq = Square{side : 6.2};  
          let rect = Rectangle{length : 3.2,breadth:5.6};  
          print_perimeter(sq,rect);  
        }  


Output:
Perimeter of a square is 24.8
Perimeter of a rectangle is 17.6

Default methods


A default method can be added to the trait definition if the definition of a method is already known.


Let's look:
trait Sample  
   
  fn a(&self);  
  fn b(&self)  
  {  
      println!("Print b");  
  }   
   

In the above case, the default behavior is added to the trait definition. We can also override the default behavior. Let' look at this scenario through an example:


 trait Sample  
{  
 fn a(&self);  
 fn b(&self)  
 {  
   println!("Print b");  
 }   
}   
  
struct Example  
{  
 a:i32,  
 b:i32,  
}  
  
  
  
impl Sample for Example  
{  
  fn a(&self)  
  {  
    println!("Value of a is {}",self.a);  
  }  
    
  fn b(&self)  
  {  
    println!("Value of b is {}",self.b);  
  }  
}  
fn main()  
{  
  let r = Example{a:5,b:7};  
  r.a();  
  r.b();    
}  

Output:
Value of a is : 5
Value of b is : 7

In the above example, the behavior of b() function is defined in the trait is overridden. Therefore, we can conclude that we can override the method which is defined in the trait.


Inheritance


The trait which is derived from another trait is known as inheritance. Sometimes, it becomes necessary to implement the trait that requires implementing another trait. If we want to derive 'B' trait from 'A' trait, then it looks like:


trait B : A;  

Let's see a simple example:
 trait A  
{  
  fn f(&self);  
}  
trait B : A  
{  
  fn t(&self);  
}  
struct Example  
{  
  first : String,  
  second : String,  
}  
impl A for Example  
{  
  fn f(&self)  
  {  
   
   print!("{} ",self.first);  
  }  
  
 }  
 impl B for Example  
 {  
  fn t(&self)  
  {  
    print!("{}",self.second);  
  }  
}  
fn main()  
{  
  let s = Example{first:String::from("javaTpoint"),second:String::from("tutorial")};  
  s.f();  
  s.t();  
}  

Output:
javaTpoint tutorial

In the above example, our program is implementing the 'B' trait. Therefore, it also requires to implement the 'A' trait. If our program does not implement the 'A' trait, then the Rust compiler throws an error.




Hi I am Pluto.