Обобщения (generics)

Дженерики позволяют писать функции и структуры, работающие с любым типом, без потери типобезопасности.

Обобщения (generics) — параметры-типы, которые подставляются при использовании, позволяя писать один код для многих типов.

Зачем нужны дженерики

Допустим, нужна функция, которая находит максимум в срезе. Без дженериков пришлось бы писать отдельную версию для i32, для f64, для char — копипаст с разными типами. Дженерики дают один код, работающий для всех типов сразу, и при этом полностью типобезопасный.

Обобщённая функция

Параметр-тип записывают в угловых скобках после имени: fn name<T>(...). Внутри T ведёт себя как обычный тип.

// T — любой тип; пара значений просто меняется местами
fn swap<T>(a: T, b: T) -> (T, T) {
    (b, a)
}

fn main() {
    let (x, y) = swap(1, 2);
    let (s, t) = swap("раз", "два");
    println!("{x} {y}");
    println!("{s} {t}");
}

Вывод:

2 1
два раз

Ограничения типов (trait bounds)

Иногда тип T должен что-то уметь. Чтобы найти максимум, элементы надо уметь сравнивать. Это выражают ограничением: T: PartialOrd означает «T должен поддерживать сравнение». Подробнее о трейтах — в следующем уроке, пока важно увидеть синтаксис.

// T должен поддерживать сравнение (PartialOrd) и копирование (Copy)
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut max = list[0];
    for &item in list {
        if item > max {
            max = item;
        }
    }
    max
}

fn main() {
    let nums = [3, 7, 2, 9, 4];
    let chars = ['a', 'z', 'm'];
    println!("{}", largest(&nums));
    println!("{}", largest(&chars));
}

Вывод:

9
z

Если бы мы передали тип, который нельзя сравнивать, код не скомпилировался бы — ограничение проверяется заранее.

Обобщённые структуры

Структуры тоже бывают обобщёнными. Например, «точка» с координатами любого числового типа.

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

impl<T: std::fmt::Display> Point<T> {
    fn show(&self) {
        println!("({}, {})", self.x, self.y);
    }
}

fn main() {
    let int_point = Point { x: 1, y: 2 };
    let float_point = Point { x: 1.5, y: 2.5 };
    int_point.show();
    float_point.show();
}

Вывод:

(1, 2)
(1.5, 2.5)

Цена дженериков — ноль

Важная деталь: дженерики в Rust бесплатны в рантайме. На этапе компиляции происходит мономорфизация — для каждого реально использованного типа генерируется своя специализированная версия кода. Результат такой же быстрый, как если бы вы написали версии вручную, но без копипаста.

Итог

  • Дженерики (<T>) дают один код для многих типов без потери типобезопасности.
  • Ограничения вида T: PartialOrd требуют, чтобы тип поддерживал нужные возможности.
  • Мономорфизация делает дженерики бесплатными в рантайме — это абстракция нулевой стоимости.
Проверьте себя
1. Зачем нужны дженерики?
AЧтобы замедлить код
BЧтобы писать один код, работающий с разными типами, без потери типобезопасности
CЧтобы отказаться от типов
DТолько для чисел
2. Что означает ограничение T: PartialOrd?
AT — это число
BТип T обязан поддерживать сравнение
CT неизменяем
DT копируется
3. Что такое мономорфизация в Rust?
AЗапрет на дженерики
BГенерация специализированной версии кода для каждого использованного типа на этапе компиляции
CЗамедление в рантайме
DСборка мусора
Поддержать проект