Generics

  • The core concept of generics is abstraction over types. They let us write one piece of code to operate with any data type without repeating ourselves to write separate versions for each type. At the compile time, Rust ensures the type safety and generates an optimized code for each concrete type used in the program.
  • Use an uppercase letter (T, U, …) or a PascalCase identifier for the data type.
    • Instead of x: u8 we use x: T.
    • Inform the compiler that T is a generic type by adding <T> at first.

With One Generic Type

struct Point<T> {
    x: T,
    y: T,
}

fn to_tuple<T>(x: T, y: T) -> (T, T) {
    (x, y)
}

fn main() {
    let a = Point { x: 0, y: 1 }; // a: Point<i32>
    let b = to_tuple(a.x, a.y); // (i32, i32)
    println!("{b:?}"); // (0, 1)

    let c = Point { x: false, y: true }; // a: Point<bool>
    let d = to_tuple(c.x, c.y); // (bool, bool)
    println!("{d:?}"); // (false, true)
}

With Multiple Generic Types

struct Point<T, U> {
    x: T,
    y: U,
}

fn to_shuffled_tuple<T, U>(x: T, y: U) -> (U, T) {
    (y, x)
}

fn main() {
    let a = Point { x: 1u8, y: true }; // a: Point<u8, bool>
    let b = to_shuffled_tuple(a.x, a.y); // (bool, u8)
    println!("{b:?}"); // (true, 1)
}

On some occasions, the compiler cannot inter the type, and we have to specify the type when using the generic type. By the way, it’s good practice to specify the type on variables when using a generic implementation.

#[derive(Debug)]
enum Data<K, V> {
    Value(V),
    KeyValue(K, V),
}

fn main() {
    let a: Data<(), bool> = Data::Value(true); // ⭐️ The compiler can not inter the type here. We have to specify the type.
    let b = Data::KeyValue(1, true); // The compiler can infer the type; i32, bool

    println!("{a:?}"); // Value(true)
    println!("{b:?}"); // KeyValue(1, true)
}

πŸ‘¨β€πŸ« Before going to the next…

  • Option and Result

    πŸ’­ This is a quick reference to Option and Result as enums. Please don’t worry too much about them for now, as we will discuss them in detail later in Error Handlingβ€”Option & Result.

    Many languages use null\ nil\ undefined types to represent empty outputs, and Exceptions to handle errors. Rust skips using both, especially to prevent issues like null pointer exceptions, sensitive data leakages through exceptions, etc. Option and Result types are two special generic enums defined in Rust’s standard library to deal with these cases.

    // An output can have either Some value or no value/ None.
    enum Option<T> { // T is a generic and it can contain any type of value.
        Some(T),
        None,
    }
    
    // A result can represent either success/ Ok or failure/ Err.
    enum Result<T, E> { // T and E are generics. T can contain any type of value, E can be any error.
        Ok(T),
        Err(E),
    }
    

    An optional value can have either Some value or no value/ None β‡’ possibility of absence

    A result can represent either success/ Ok or failure/ Err β‡’ possibility of failure

    • Option

      struct Task {
          title: String,
          assignee: Option<Person>, // πŸ’‘ Instead of `assignee: Person`, we use `assignee: Option<Person>` as the assignee can be `None`.
      }
      
      fn get_id_by_username(username: &str) -> Option<usize> { // πŸ’‘ Instead of setting return type as `usize`, set it `Option<usize>`
          // if username can be found in the system, return userId
              return Some(userId); // πŸ’‘ Instead of return userId, return Some(userId)
      
          // else
              None // πŸ’‘ The last return statement no need `return` keyword and ending `;`
      }
      
      fn main() {
          let username = "anonymous";
          match get_id_by_username(username) { // πŸ’‘ We can use pattern matching to catch the relevant return type (Some/None)
              None => println!("User not found"),
              Some(i) => println!("User Id: {}", i),
          }
      }
      
    • Result

      fn get_word_count_from_file(file_name: &str) -> Result<u32, &str> { // πŸ’‘ Instead of setting return type as `u32`, set it `Result<u32, &str>`
          // if the file is not found on the system, return error
              return Err("File can not be found!"); // πŸ’‘ Instead panic/ break when the file can not be found; return Err(something)
      
          // else, count and return the word count
              Ok(word_count) // πŸ’‘ Instead of return `word_count`, return `Ok(word_count)`
              // πŸ’‘ The last return statement no need `return` keyword and ending `;`
      }
      
      fn main() {
          let mut file_name = "file_a";
          match get_word_count_from_file(file_name) { // πŸ’‘ We can use pattern matching to catch the relevant return type (Ok/Err)
              Ok(i) => println!("Word Count: {}", i),
              Err(e) => println!("Error: {}", e)
          }
      }