Обобщённые типы, трейты и времена жизни
В любом языке программирования есть инструменты, которые помогают бороться с дублированием. В Rust один из таких инструментов — обобщения (generics): абстрактные заменители конкретных типов или других свойств. Мы можем описать поведение обобщений и их связь с другими обобщениями, не зная, что именно окажется на их месте при компиляции и запуске кода.
Функции могут принимать параметры не конкретного типа вроде i32 или
String, а какого-то обобщённого — точно так же, как они принимают параметры
с заранее неизвестными значениями, чтобы выполнять один и тот же код для
разных значений. На самом деле обобщениями мы уже пользовались: в главе 6 —
Option<T>, в главе 8 — Vec<T> и HashMap<K, V>, в главе 9 —
Result<T, E>. В этой главе вы научитесь определять с помощью обобщений
собственные типы, функции и методы!
Сначала мы вспомним, как вынести код в функцию, чтобы избавиться от дублирования. Затем тем же приёмом сделаем из двух функций, которые отличаются только типами параметров, одну обобщённую. Ещё мы объясним, как использовать обобщённые типы в определениях структур и перечислений.
Потом вы узнаете, как с помощью трейтов описывать поведение обобщённо. Трейты можно сочетать с обобщёнными типами, чтобы обобщённый тип принимал не какой угодно тип, а только такой, у которого есть определённое поведение.
И наконец, мы обсудим времена жизни (lifetimes) — разновидность обобщений, которая сообщает компилятору, как ссылки связаны друг с другом. Времена жизни позволяют дать компилятору достаточно сведений о заимствованных значениях, чтобы он мог убедиться в действительности ссылок в большем числе ситуаций, чем без нашей помощи.
Убираем дублирование, вынося код в функцию
Обобщения позволяют заменить конкретные типы заполнителем, который обозначает сразу много типов, и так избавиться от дублирования кода. Но прежде чем погружаться в синтаксис обобщений, посмотрим, как убрать дублирование без обобщённых типов: вынести код в функцию, заменив конкретные значения заполнителем, обозначающим много значений. А затем тем же приёмом выделим обобщённую функцию! Научившись замечать повторяющийся код, который можно вынести в функцию, вы начнёте замечать и повторяющийся код, где пригодятся обобщения.
Начнём с короткой программы из листинга 10-1, которая находит наибольшее число в списке.
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let mut largest = &number_list[0];
for number in &number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {largest}");
assert_eq!(*largest, 100);
}
Мы храним список целых чисел в переменной number_list и кладём ссылку на
первое число списка в переменную largest. Затем перебираем все числа
списка, и если текущее число больше того, на которое указывает largest,
заменяем ссылку в этой переменной. Если же текущее число меньше или равно
наибольшему из встреченных, переменная не меняется, и код переходит к
следующему числу. Когда все числа перебраны, largest указывает на наибольшее
— в нашем случае на 100.
Теперь нам поручили найти наибольшее число в двух разных списках. Можно скопировать код из листинга 10-1 и использовать ту же логику в двух местах программы, как в листинге 10-2.
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let mut largest = &number_list[0];
for number in &number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {largest}");
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let mut largest = &number_list[0];
for number in &number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {largest}");
}
Этот код работает, но копировать код утомительно, и легко ошибиться. К тому же при любом изменении придётся не забыть обновить код в нескольких местах.
Чтобы избавиться от дублирования, создадим абстракцию — функцию, которая работает с любым списком целых чисел, переданным ей параметром. Так код станет понятнее, а идея «найти наибольшее число в списке» будет выражена абстрактно.
В листинге 10-3 мы выносим код поиска наибольшего числа в функцию largest.
Затем вызываем её, чтобы найти наибольшее число в двух списках из листинга
10-2. Эту функцию можно будет применить и к любому другому списку значений
i32, который может появиться в будущем.
fn largest(list: &[i32]) -> &i32 {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {result}");
assert_eq!(*result, 100);
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let result = largest(&number_list);
println!("The largest number is {result}");
assert_eq!(*result, 6000);
}
У функции largest есть параметр list — любой конкретный срез значений
i32, который мы можем передать в функцию. Поэтому при вызове функции код
выполняется для тех конкретных значений, которые мы передали.
Подведём итог: чтобы превратить код из листинга 10-2 в код из листинга 10-3, мы сделали следующее:
- Нашли повторяющийся код.
- Перенесли повторяющийся код в тело функции, а в сигнатуре функции указали, что этот код принимает на вход и что возвращает.
- Заменили оба места с повторяющимся кодом вызовами функции.
Дальше мы проделаем те же шаги, но уже с обобщениями. Точно так же, как тело
функции может работать с абстрактным list вместо конкретных значений,
обобщения позволяют коду работать с абстрактными типами.
Допустим, у нас две функции: одна находит наибольший элемент в срезе значений
i32, а другая — в срезе значений char. Как избавиться от такого
дублирования? Давайте выясним!