Generics в Rust: зачем нужны и как использовать

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в Generics в Rust

Обобщённое программирование (generics) — одна из ключевых концепций Rust, позволяющая писать гибкий, переиспользуемый и безопасный код. Если вы переходите на Rust с других языков (например, C++ или Java), то наверняка сталкивались с шаблонами или дженериками. В Rust generics работают аналогично, но с особым акцентом на производительность и безопасность памяти.

Зачем нужны generics? Представьте, что вам нужно написать функцию, которая находит максимальный элемент в списке. Без generics вам пришлось бы писать отдельную версию для i32, f64, String и каждого пользовательского типа. С generics вы пишете одну функцию, которая работает с любым типом, поддерживающим сравнение.

В этой статье мы разберём, что такое generics в Rust, как их использовать с функциями, структурами и трейтами, а также рассмотрим практические примеры.

Основы Generics: функции и структуры

Обобщённые функции

Самый простой способ познакомиться с generics — написать обобщённую функцию. Синтаксис использует угловые скобки <T>, где T — это параметр типа (type parameter).

fn largest<T>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

fn main() {
    let numbers = vec![34, 50, 25, 100, 65];
    let result = largest(&numbers);
    println!("Наибольшее число: {}", result);

    let chars = vec!['y', 'm', 'a', 'q'];
    let result = largest(&chars);
    println!("Наибольший символ: {}", result);
}

Однако этот код не скомпилируется! Rust требует, чтобы тип T реализовывал трейт PartialOrd (для сравнения). Ограничения (trait bounds) мы рассмотрим позже, а пока запомните: generics позволяют абстрагироваться от конкретного типа, но компилятор должен знать, какие операции допустимы.

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

Generics работают не только с функциями, но и с пользовательскими типами данных. Например, точка с координатами может хранить целые числа или числа с плавающей точкой:

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

fn main() {
    let integer_point = Point { x: 5, y: 10 };
    let float_point = Point { x: 1.0, y: 4.0 };
    println!("Целочисленная точка: ({}, {})", integer_point.x, integer_point.y);
    println!("Точка с плавающей точкой: ({}, {})", float_point.x, float_point.y);
}

Можно использовать несколько параметров типа, если поля имеют разные типы:

struct Pair<T, U> {
    first: T,
    second: U,
}

fn main() {
    let pair = Pair { first: 42, second: "ответ" };
    println!("Пара: {} и {}", pair.first, pair.second);
}

Generics с трейтами и ограничениями (Trait Bounds)

Что такое trait bounds?

Trait bounds — это способ сообщить компилятору, что обобщённый тип должен реализовывать определённые трейты. Без них мы не можем вызывать методы на обобщённом значении. Вернёмся к функции largest и исправим её:

use std::cmp::PartialOrd;

fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

fn main() {
    let numbers = vec![34, 50, 25, 100, 65];
    println!("Наибольшее: {}", largest(&numbers));
}

Теперь функция принимает только те типы, которые поддерживают сравнение (PartialOrd).

Множественные ограничения

Вы можете указать несколько трейтов через +. Например, тип должен быть одновременно сравнимым и клонируемым:

use std::fmt::Display;

fn print_and_compare<T: PartialOrd + Display>(a: &T, b: &T) {
    println!("Сравниваем {} и {}", a, b);
    if a > b {
        println!("{} больше", a);
    } else {
        println!("{} больше или равно", b);
    }
}

fn main() {
    print_and_compare(&10, &20);
}

Синтаксис where

Когда ограничений становится много, удобнее использовать синтаксис where:

use std::fmt::Debug;

fn complex_function<T, U>(a: &T, b: &U) 
where
    T: PartialOrd + Debug,
    U: PartialOrd + Debug,
{
    println!("a = {:?}, b = {:?}", a, b);
    // ...
}

Реализация методов для обобщённых структур

Вы можете реализовать методы для обобщённых структур, используя impl<T>:

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

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

// Специализация для конкретного типа
impl Point<f64> {
    fn distance_from_origin(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    let p = Point { x: 3.0, y: 4.0 };
    println!("x = {}", p.x());
    println!("Расстояние до начала координат: {}", p.distance_from_origin());
}

Обратите внимание: метод distance_from_origin доступен только для Point<f64>. Это называется мономорфизацией — компилятор создаёт отдельную версию кода для каждого используемого типа.

Производительность Generics: мономорфизация

Одно из главных преимуществ generics в Rust — они не создают накладных расходов во время выполнения. В отличие от дженериков в Java (которые используют стирание типов), Rust компилирует обобщённый код для каждого конкретного типа отдельно. Этот процесс называется мономорфизацией (monomorphization).

Пример: если вы используете Vec<i32> и

Блоги

Рекомендаций по книгам