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

Каждый язык программирования имеет в своём арсенале эффективные средства борьбы с дублированием кода. В Rust одним из таких инструментов являются обобщённые типы данных - generics. Это абстрактные подставные типы на место которых возможно поставить какой-либо конкретный тип или другое свойство. Когда мы пишем код, мы можем выразить поведение обобщённых типов или их связь с другими обобщёнными типами, не зная какой тип будет использован на их месте при компиляции и запуске кода.

Это подобно тому, как функция принимает на вход параметры с разными заранее неизвестными значениями и запускает на них одинаковый код. Функции могут принимать параметры некоторого "обобщённого" типа вместо конкретного типа, вроде i32 или String. Мы уже использовали такие типы данных в Главе 6 (Option<T>), в Главе 8 (Vec<T> и HashMap<K, V>) и в Главе 9 (Result<T, E>). В этой главе мы рассмотрим, как определить наши собственные типы данных, функции и методы, используя возможности обобщённых типов.

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

Затем вы изучите как использовать типажи (traits) для определения поведения в обобщённом виде. Можно комбинировать типажи с обобщёнными типами для ограничения обобщённого типа только теми типами, которые имеют определённое поведение, в отличии от любых типов.

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

Удаление дублирования кода с помощью выделения общей функциональности

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

Рассмотрим небольшую программу, которая ищет наибольшее число в списке, как показано в листинге 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-1, который находит наибольшее число только в одном конкретном списке, новая программа может искать наибольшее число в двух разных списках.

Файл: 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, которые мы можем передать в неё. В результате вызова функции, код выполнится с конкретными, переданными в неё значениями. Не беспокойтесь о синтаксисе цикла for на данный момент. Мы не ссылаемся здесь на ссылку на i32; мы сопоставляем шаблон и деструктурируем каждый &i32 который получает цикл for по этой причине item будет типа i32 внутри тела цикла. Мы подробно рассмотрим сопоставление с образцом в Главе 18.

Итак, вот шаги выполненные для изменения кода из листинга 10-2 в листинг 10-3:

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

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

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