Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Обобщённые типы, трейты и времена жизни

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

Функции могут принимать параметры не конкретного типа вроде i32 или String, а какого-то обобщённого — точно так же, как они принимают параметры с заранее неизвестными значениями, чтобы выполнять один и тот же код для разных значений. На самом деле обобщениями мы уже пользовались: в главе 6 — Option<T>, в главе 8 — Vec<T> и HashMap<K, V>, в главе 9 — Result<T, E>. В этой главе вы научитесь определять с помощью обобщений собственные типы, функции и методы!

Сначала мы вспомним, как вынести код в функцию, чтобы избавиться от дублирования. Затем тем же приёмом сделаем из двух функций, которые отличаются только типами параметров, одну обобщённую. Ещё мы объясним, как использовать обобщённые типы в определениях структур и перечислений.

Потом вы узнаете, как с помощью трейтов описывать поведение обобщённо. Трейты можно сочетать с обобщёнными типами, чтобы обобщённый тип принимал не какой угодно тип, а только такой, у которого есть определённое поведение.

И наконец, мы обсудим времена жизни (lifetimes) — разновидность обобщений, которая сообщает компилятору, как ссылки связаны друг с другом. Времена жизни позволяют дать компилятору достаточно сведений о заимствованных значениях, чтобы он мог убедиться в действительности ссылок в большем числе ситуаций, чем без нашей помощи.

Убираем дублирование, вынося код в функцию

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

Начнём с короткой программы из листинга 10-1, которая находит наибольшее число в списке.

Файл: src/main.rs
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);
}
Листинг 10-1: Ищем наибольшее число в списке чисел

Мы храним список целых чисел в переменной number_list и кладём ссылку на первое число списка в переменную largest. Затем перебираем все числа списка, и если текущее число больше того, на которое указывает largest, заменяем ссылку в этой переменной. Если же текущее число меньше или равно наибольшему из встреченных, переменная не меняется, и код переходит к следующему числу. Когда все числа перебраны, largest указывает на наибольшее — в нашем случае на 100.

Теперь нам поручили найти наибольшее число в двух разных списках. Можно скопировать код из листинга 10-1 и использовать ту же логику в двух местах программы, как в листинге 10-2.

Файл: src/main.rs
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-2: Код, который ищет наибольшее число в двух списках чисел

Этот код работает, но копировать код утомительно, и легко ошибиться. К тому же при любом изменении придётся не забыть обновить код в нескольких местах.

Чтобы избавиться от дублирования, создадим абстракцию — функцию, которая работает с любым списком целых чисел, переданным ей параметром. Так код станет понятнее, а идея «найти наибольшее число в списке» будет выражена абстрактно.

В листинге 10-3 мы выносим код поиска наибольшего числа в функцию largest. Затем вызываем её, чтобы найти наибольшее число в двух списках из листинга 10-2. Эту функцию можно будет применить и к любому другому списку значений i32, который может появиться в будущем.

Файл: src/main.rs
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);
}
Листинг 10-3: Абстрагированный код поиска наибольшего числа в двух списках

У функции largest есть параметр list — любой конкретный срез значений i32, который мы можем передать в функцию. Поэтому при вызове функции код выполняется для тех конкретных значений, которые мы передали.

Подведём итог: чтобы превратить код из листинга 10-2 в код из листинга 10-3, мы сделали следующее:

  1. Нашли повторяющийся код.
  2. Перенесли повторяющийся код в тело функции, а в сигнатуре функции указали, что этот код принимает на вход и что возвращает.
  3. Заменили оба места с повторяющимся кодом вызовами функции.

Дальше мы проделаем те же шаги, но уже с обобщениями. Точно так же, как тело функции может работать с абстрактным list вместо конкретных значений, обобщения позволяют коду работать с абстрактными типами.

Допустим, у нас две функции: одна находит наибольший элемент в срезе значений i32, а другая — в срезе значений char. Как избавиться от такого дублирования? Давайте выясним!