Обобщения (generics) в Rust
Ты уже пользовался обобщениями, даже если не знал, как они называются: и Vec<i32>, и Option<T> устроены именно так. На этой странице ты научишься писать свои обобщённые функции и структуры — обобщения (generics).
Какую проблему решают обобщения
Заголовок раздела «Какую проблему решают обобщения»Допустим, тебе нужна функция, которая находит наибольшее число в списке, причём и для i32, и для f64. Без обобщений её пришлось бы написать дважды:
fn largest_i32(numbers: &[i32]) -> i32 { let mut largest = numbers[0]; for &n in numbers { if n > largest { largest = n; } } largest}
fn largest_f64(numbers: &[f64]) -> f64 { let mut largest = numbers[0]; for &n in numbers { if n > largest { largest = n; } } largest}Логика одна и та же, просто скопирована для каждого типа. Обобщения позволяют написать её один раз.
Обобщённая функция
Заголовок раздела «Обобщённая функция»fn largest<T: PartialOrd + Copy>(numbers: &[T]) -> T { let mut largest = numbers[0]; for &n in numbers { if n > largest { largest = n; } } largest}
fn main() { let integers = [10, 25, 3, 47, 8]; let floats = [1.5, 2.8, 0.3];
println!("{}", largest(&integers)); println!("{}", largest(&floats));}Вывод:
472.8<T: PartialOrd + Copy> читается так: «для любого типа T, если его можно сравнивать (PartialOrd) и копировать (Copy)». T — это заполнитель: Rust создаёт версию функции для того конкретного типа, с которым ты её вызываешь (i32, f64 и так далее).
Обобщённая структура
Заголовок раздела «Обобщённая структура»Структуры тоже бывают обобщёнными:
struct Pair<T> { first: T, second: T,}
fn main() { let numbers = Pair { first: 5, second: 10 }; let words = Pair { first: "привет", second: "мир" };
println!("{} {}", numbers.first, numbers.second); println!("{} {}", words.first, words.second);}Вывод:
5 10привет мирОдно определение Pair<T> подходит для чисел, текста и любого другого типа: numbers — это Pair<i32>, а words — Pair<&str>, но саму структуру ты написал только один раз.
Стоит ли это чего-нибудь во время работы программы?
Заголовок раздела «Стоит ли это чего-нибудь во время работы программы?»Нет. Rust создаёт отдельную, специализированную версию функции или структуры для каждого конкретного типа, который ты используешь (это называется мономорфизацией, monomorphization). Когда программа запускается, обобщённого кода в ней уже нет — только обычные функции с конкретными типами, ровно такие же быстрые, как если бы ты написал каждую вручную.
Методы обобщённой структуры
Заголовок раздела «Методы обобщённой структуры»Методы к обобщённой структуре добавляются блоком impl, точно как на странице Структуры. Новое здесь только одно: у impl тоже должен быть свой <T>:
struct Pair<T> { first: T, second: T,}
impl<T> Pair<T> { fn new(first: T, second: T) -> Self { Pair { first, second } }
fn swap(self) -> Pair<T> { Pair { first: self.second, second: self.first } }}
impl Pair<f64> { fn average(&self) -> f64 { (self.first + self.second) / 2.0 }}
fn main() { let words = Pair::new("левый", "правый").swap(); println!("{} {}", words.first, words.second);
let scores = Pair::new(7.0, 9.0); println!("{}", scores.average());}Вывод:
правый левый8Здесь два вида блоков impl:
impl<T> Pair<T>означает: «эти методы есть уPairлюбого типа». Первый<T>читай как «для любого типаT». Без него Rust решит, чтоT— имя настоящего типа, и пожалуется, что не может его найти.impl Pair<f64>без<T>. Его методы есть только уPair<f64>. Поэтомуaverageможет использовать+и/ 2.0. Попробуй вызватьwords.average()— компилятор скажет, что уPair<&str>такого метода нет.
У структуры может быть и несколько параметров типа, например struct Pair<T, U> { first: T, second: U }, если два поля могут быть разных типов. А когда ограничения трейтов становятся длинными, их можно вынести в блок where, чтобы сигнатура оставалась читаемой. Об этом — на странице Продвинутые трейты.
Практика
Заголовок раздела «Практика»1. Обобщённый минимум
Заголовок раздела «1. Обобщённый минимум»Напиши fn smallest<T: PartialOrd + Copy>(items: &[T]) -> T, которая возвращает наименьший элемент. Проверь её на [8, 3, 5] и на символах ['r', 'u', 's', 't']. Подсказка: это функция largest с этой страницы, в которой перевёрнуто одно сравнение.
Показать решение
fn smallest<T: PartialOrd + Copy>(items: &[T]) -> T { let mut smallest = items[0]; for &item in items { if item < smallest { smallest = item; } } smallest}
fn main() { println!("{}", smallest(&[8, 3, 5])); println!("{}", smallest(&['r', 'u', 's', 't']));}Вывод:
3r2. Большая половина Pair
Заголовок раздела «2. Большая половина Pair»Добавь к Pair<T> метод larger(&self) -> &T, который возвращает ссылку на то поле, что больше. Проверь его на Pair чисел и на Pair слов. Подсказка: для сравнения нужен PartialOrd, поэтому поставь ограничение прямо на impl: impl<T: PartialOrd> Pair<T>.
Слова здесь английские: строки сравниваются по алфавиту, и так результат совпадёт с английской версией урока.
Показать решение
struct Pair<T> { first: T, second: T,}
impl<T: PartialOrd> Pair<T> { fn larger(&self) -> &T { if self.first > self.second { &self.first } else { &self.second } }}
fn main() { let numbers = Pair { first: 3, second: 9 }; let words = Pair { first: "pear", second: "apple" };
println!("{}", numbers.larger()); println!("{}", words.larger());}Вывод:
9pear- Обобщения позволяют написать одну функцию или структуру для нескольких типов, а не копировать код.
<T>— тип-заполнитель; ограничения трейтов (T: PartialOrd) задают, каким может бытьT.impl<T> Pair<T>добавляет методы для любогоT;impl Pair<f64>— только для одного конкретного типа.- Обобщённый код компилируется в отдельные версии для каждого типа — во время работы программы это ничего не стоит.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Какую проблему решают обобщения?
2. Что делает PartialOrd + Copy в fn largest<T: PartialOrd + Copy>(...)?
3. Стоят ли обобщения чего-нибудь во время работы программы по сравнению с кодом, написанным вручную?
4. Что позволяет одно определение struct Pair<T>?
Результат: 0 / 4
Что почитать
Заголовок раздела «Что почитать»- Обобщённые типы данных — книга «Язык программирования Rust» (перевод на русский): обобщения в функциях, структурах, перечислениях и методах, включая пример с
largest. - Обобщения — Rust на примерах (перевод на русский): короткие запускаемые примеры обобщённых функций и структур и их синтаксиса.
- Реализация — Rust на примерах (перевод на русский): подробнее о блоках
impl<T>и о методах для одного конкретного типа. - Ограничения — Rust на примерах (перевод на русский): как ограничения трейтов сужают, каким может быть обобщённый тип, и зачем они нужны.
- Generic parameters — The Rust Reference (на английском): точные правила, когда нужны все подробности.