Generics

Usage

Generics нужны для того, чтобы не повторять один и тот-же код несколько раз для разных типов данных. Это один из трёх основных способов экономии на повторении кода наравне с макросами и интерфейсами traits. Пример: нужно сортировать 2 набора данных - массив чисел, массив символов.

fn largest_i32(n_list: &[i32]) -> &i32 {
    let mut largest_num = &n_list[0];
    for i in n_list {
        if largest_num < i {
            largest_num = i;
        }
    }
    largest_num
}

fn largest_char(n_list: &[char]) -> &char {
    let mut largest_char = &n_list[0];
    for i in n_list {
        if largest_char < i {
            largest_char = i;
        }
    }
    largest_char

Вместо того, чтобы писать 2 почти идентичные функции под 2 типа данных, можно объединить оба типа в 1 функцию, указав “неопределённый тип”.

Note

Нужно учесть, что не все типы данных имеют возможность сравнения объектов между собой, возможность выстроить их в порядок (Order). Поэтому в примере ниже надо дополнить тип в заголовке интерфейсом-свойством (trait) порядка.

fn largest_universal<T: std::cmp::PartialOrd>(list: &[T]) -> &T {  
    // <T> = неопределённый тип, со свойством упорядочивания PartialOrd
    let mut largest = &list[0];  
  
    for item in list {  
        if largest < item {  
            largest = item;  
        }  
    }  
    largest  
}

fn main() {  
    let num_list = vec![11, 6, 33, 56, 13];//упорядочить числа 
    println!("Largest number: {}", largest_universal(&num_list));  
  
    let char_list = vec!['y','m','a','q'];//упорядочить символы в той же функции  
    println!("Largest char: {}", largest_universal(&char_list));
}

Структуры с неопределёнными типами

Можно создавать структуры, тип данных которых заранее неопределён. Причём в одной структуре можно сделать несколько разных типов.

struct Point<T,U> {  // <T> и <U> - 2 разных типа
    x: T,  
    y: U,  
}

let integer = Point{x:5,y:6};  // в оба поля пишем числа типа i32
let float_int = Point{x:1,y:4.2}; // в поля пишем разные типы i32 и f32

Аналогично неопределённые типы можно делать в перечислениях:

enum Option<T> {
    Some(T), // <T> - возможное значение любого типа, которое может быть или нет
    None,
}

enum Result<T, E> {
    Ok(T), // T - тип результата успешной операции
    Err(E), // Е - тип ошибки
}

Методы со структурами с неопределёнными типами

Можно прописать методы над данными неопределённого типа. Важно в заголовке метода указывать при этом неопределённый тип, чтобы помочь компилятору отделить его от обычного типа данных. Можно при этом сделать реализацию для конкретных типов данных, например, расчёт расстояния до точки от центра координат будет работать только для чисел с плавающей точкой. Для других типов данных метод расчёта работать не будет:

struct Point<T> {  
    x: T,  
    y: T,  
}  
  
impl<T> Point<T> {  // указываем неопределённый тип в заголовке
    fn show_x(&self) -> &T {  // метод возвращает поле данных
        &self.x  
    } }

impl Point<f32> {  // указываем конкретный тип float в методе, 
// чтобы только для него реализовать расчёт. Для не-float метод не будет работать
    fn distance_from_origin(&self) -> f32 {  
        (self.x.powi(2) + self.y.powi(2)).sqrt()  
    } }
  
fn main() {  
    let p = Point{x:5,y:6};  
    println!("P.x = {}",p.show_x()); // вызов метода для экземпляра p
}
Tip

Код с неопределёнными типами не замедляет производительность, потому что компилятор проходит и подставляет конкретные типы везде, где видит generics (“monomorphization” of code).