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

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

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

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

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

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

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

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

Filename: 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);
}
Listing 10-1: Поиск наибольшего числа в списке чисел

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

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

Filename: 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}");
}
Listing 10-2: Код для поиска наибольшего числа в двух списках чисел

Хотя этот код работает, дублирование кода утомительно и чревато ошибками. Кроме того, когда мы хотим изменить код, нам нужно не забыть обновить его в нескольких местах.

Чтобы устранить это дублирование, мы создадим абстракцию, определив функцию, которая работает с любым списком целых чисел, переданным в качестве параметра. Это решение делает наш код понятнее и позволяет выразить понятие поиска наибольшего числа в списке абстрактно.

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

Filename: 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);
}
Listing 10-3: Абстрагированный код для поиска наибольшего числа в двух списках

Функция largest имеет параметр с именем list, который представляет любой конкретный срез значений i32, передаваемый в функцию. В результате, когда мы вызываем функцию, код выполняется для конкретных значений, которые мы передаем.

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

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

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

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