Generics are a facility to write code for multiple contexts with different types. In Rust, generics refer to the parameterization of data types and traits. Generics allows to write more concise and clean code by reducing code duplication and providing type-safety. The concept of Generics can be applied to methods, functions, structures, enumerations, collections and traits.
The <T> syntax known as the type parameter, is used to declare a generic construct. T represents any data-type.
The following example declares a vector that can store only integers.
fn main(){ let mut vector_integer: Vec<i32> = vec![20,30]; vector_integer.push(40); println!("{:?}",vector_integer); }
[20, 30, 40]
Consider the following snippet −
fn main() { let mut vector_integer: Vec<i32> = vec![20,30]; vector_integer.push(40); vector_integer.push("hello"); //error[E0308]: mismatched types println!("{:?}",vector_integer); }
The above example shows that a vector of integer type can only store integer values. So, if we try to push a string value into the collection, the compiler will return an error. Generics make collections more type safe.
The type parameter represents a type, which the compiler will fill in later.
struct Data<T> { value:T, } fn main() { //generic type of i32 let t:Data<i32> = Data{value:350}; println!("value is :{} ",t.value); //generic type of String let t2:Data<String> = Data{value:"Tom".to_string()}; println!("value is :{} ",t2.value); }
The above example declares a generic structure named Data. The <T> type indicates some data type. The main() function creates two instances − an integer instance and a string instance, of the structure.
value is :350 value is :Tom
Traits can be used to implement a standard set of behaviors (methods) across multiple structures. Traits are like interfaces in Object-oriented Programming. The syntax of trait is as shown below −
trait some_trait { //abstract or method which is empty fn method1(&self); // this is already implemented , this is free fn method2(&self){ //some contents of method2 } }
Traits can contain concrete methods (methods with body) or abstract methods (methods without a body). Use a concrete method if the method definition will be shared by all structures implementing the Trait. However, a structure can choose to override a function defined by the trait.
Use abstract methods if the method definition varies for the implementing structures.
impl some_trait for structure_name { // implement method1() there.. fn method1(&self ){ } }
The following examples defines a trait Printable with a method print(), which is implemented by the structure book.
fn main(){ //create an instance of the structure let b1 = Book { id:1001, name:"Rust in Action" }; b1.print(); } //declare a structure struct Book { name:&'static str, id:u32 } //declare a trait trait Printable { fn print(&self); } //implement the trait impl Printable for Book { fn print(&self){ println!("Printing book with id:{} and name {}",self.id,self.name) } }
Printing book with id:1001 and name Rust in Action
The example defines a generic function that displays a parameter passed to it. The parameter can be of any type. The parameter’s type should implement the Display trait so that its value can be printed by the println! macro.
use std::fmt::Display; fn main(){ print_pro(10 as u8); print_pro(20 as u16); print_pro("Hello Howcodex"); } fn print_pro<T:Display>(t:T){ println!("Inside print_pro generic function:"); println!("{}",t); }
Inside print_pro generic function: 10 Inside print_pro generic function: 20 Inside print_pro generic function: Hello Howcodex