Skip to content

Latest commit

 

History

History
executable file
·
147 lines (116 loc) · 3.87 KB

File metadata and controls

executable file
·
147 lines (116 loc) · 3.87 KB
title Generics
slug generics
  • Rust let us write one piece of code to operate with multiple data type via generics, without repeating ourselves to write separate versions for each type.
  • 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.

Tip

  • Rust generics represent an abstraction over types.
  • As assembly language is inherently non-generic, the Rust compiler uses monomorphization to generate distinct, concrete machine code for each type used.

With One 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);     // b: (i32, i32)
    
    let c = Point { x: false, y: true };    // c: Point<bool>
    let d = to_tuple(c.x, c.y);             // d: (bool, bool)

    println!("{b:?}"); // (0, 1)
    println!("{d:?}"); // (false, true)
}

// 💡 We can achieve the same functionality by destructures.
// let b = {
//     let Point { x, y } = a;
//     (x, y)
// };

With Multiple 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);    // b: (bool, u8)

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

// 💡 We can achieve the same functionality by destructures.
// let b = {
//     let Point { x, y } = a;
//     (y, x)
// };

With Enums

enum Data<K, V> {
    Value(V),
    KeyValue(K, V),
}

fn main() {
    let data = vec![
        Data::KeyValue("Steve".to_string(), 10),
        Data::Value(20),
        Data::KeyValue("Tom".to_string(), 30),
        Data::Value(40),
        Data::KeyValue("Mike".to_string(), 50),
    ];

    for item in data {
        match item {
            Data::KeyValue(k, v) => println!("{k}: {v}"),
            Data::Value(v) => println!("Unknown: {v}"),
        }
    }
}

Tip

On some occasions, the compiler cannot infer the type, and we have to specify the type when using the generic type.
However, 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 infer the type here
    let b = Data::KeyValue(1, true);            // The compiler can infer the type here

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

👨‍🏫 Before going to the next...

  • Option and Result

    [!recap] This is a quick reference to Option and Result as generic enums. So, please don’t worry too much about them for now. We will discuss them in detail under Error Handling: Option and Result.

    Many programming languages use exceptions to handle errors and null\ nil\ undefined types to handle missing values. Historically, this decision has led to severe runtime issues (such as null pointer exceptions) and security vulnerabilities (sensitive data leakages, through error traces and exceptions). Rust skip both and provide two special generic enums defined in its standard library to prevent these issues.

    • Option

      • Represents the potential absence of a value.
      • The value can have either some value/Some or no value/None.
      enum Option<T> {
          Some(T),
          None,
      }
    • Result

      • Represents the outcome of a fallible operation.
      • The result can have either success/Ok or failure/Err.
      enum Result<T, E> {
          Ok(T),
          Err(E),
      }