Введение: что такое Generics и зачем они в Rust?
Представьте, что вам нужно написать функцию, которая находит наибольший элемент в списке. Для целых чисел — одна функция, для чисел с плавающей точкой — другая, для строк — третья. Код будет дублироваться, а поддерживать его станет сложно. В Rust эту проблему решают Generics (обобщённое программирование).
Generics позволяют писать код, который работает с разными типами данных, не теряя при этом строгой типизации. Вместо того чтобы дублировать функции для каждого типа, вы пишете одну обобщённую версию. Rust на этапе компиляции подставляет конкретный тип и проверяет корректность. Это даёт:
- Переиспользование кода — одна функция для всех типов.
- Безопасность — все ошибки типов отлавливаются на этапе компиляции.
- Производительность — благодаря мономорфизации (компилятор генерирует отдельный код для каждого использованного типа) обобщённый код работает так же быстро, как и специализированный.
В этой статье мы разберём, как использовать Generics в Rust на практических примерах, начиная с простых функций и заканчивая структурами и трейтами.
1. Generics в функциях: первый шаг к универсальности
Синтаксис и базовый пример
Чтобы сделать функцию обобщённой, нужно указать параметр типа в угловых скобках <T>. Обычно используют заглавные буквы: T, U, V. Вот как выглядит функция, возвращающая переданное значение:
fn identity<T>(value: T) -> T {
value
}
fn main() {
let num = identity(42);
let text = identity("Hello");
println!("{} {}", num, text); // Выведет: 42 Hello
}
Компилятор сам выводит тип на основе переданного аргумента. Вы также можете указать тип явно: identity::<i32>(42).
Ограничения: трейты как границы типов
Что если мы хотим сравнивать значения? Просто написать value1 > value2 не получится — не все типы поддерживают сравнение. Нужно указать, что тип должен реализовывать трейт 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));
let chars = vec!['y', 'm', 'a', 'q'];
println!("Наибольший символ: {}", largest(&chars));
}
Здесь T: PartialOrd — это ограничение трейта. Оно гарантирует, что для типа T определён оператор сравнения. Без этого ограничения код не скомпилируется.
2. Generics в структурах и перечислениях
Структуры с обобщёнными полями
Generics отлично работают и с пользовательскими типами. Например, структура Point может хранить координаты любого типа:
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 };
// let mixed = Point { x: 5, y: 4.0 }; // Ошибка! Оба поля должны быть одного типа
}
Если нужно, чтобы поля могли быть разных типов, используйте несколько параметров:
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let mixed = Point { x: 5, y: 4.0 }; // Теперь работает
}
Перечисления с Generics
Стандартное перечисление Option<T> — отличный пример обобщённого перечисления. Вы можете создать своё:
enum Result<T, E> {
Ok(T),
Err(E),
}
// Использование
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Result::Err("Деление на ноль".to_string())
} else {
Result::Ok(a / b)
}
}
3. Generics и трейты: мощная комбинация
Реализация методов для обобщённых структур
Вы можете реализовать методы для структуры с Generics, указав параметр типа после impl:
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!("Расстояние: {}", p.distance_from_origin());
}
Обратите внимание: impl<T> Point<T> — это реализация для всех типов T, а impl Point<f64> — только для f64.
Трейты с Generics и where-клаузы
При сложных ограничениях удобно использовать where:
use std::fmt::Display;
fn print_pair<T, U>(a: T, b: U)
where
T: Display,
U: Display + Clone,
{
println!("Первый: {}, Второй: {}", a, b);
let _cloned_b = b.clone(); // Только если U реализует Clone
}
Синтаксис where делает код более читаемым, особенно когда много ограничений.
4. Продвинутые приёмы: ассоциированные типы и PhantomData
Ассоциированные типы в трейтах
Иногда трейту нужен не параметр, а внутренний тип, который определяется реализа