Узагальнені типи, трейти та лайфтайми

Кожна мова програмування має інструменти, щоб уникати повторення концепцій. У мові Rust, одним з таких інструментів є узагальнені типи, також відомі як <0>дженеріки</0> (від англ. <0>generic</0> "загальний, типовий"): абстрактні замінники конкретних типів або інших властивостей. Ми можемо описувати поведінку узагальнених типів і їх відношення до інших узагальнених типів, не знаючи, який саме тип буде на їх місці під час компіляції і виконання коду.

Функції можуть приймати параметри певного узагальненого типу замість конкретного типу (наприклад, i32 або String), так само як функції можуть приймати параметри з невідомими значеннями і виконувати той самий код з багатьма конкретними значеннями. Насправді ми вже стикалися з узагальненими типами у розділі 6 (Option<T>), розділі 8 (Vec<T> та HashMap<K, V>) і розділі 9 (Result<T, E>). У цьому розділі ми побачимо, як можна визначати ваші власні типи, функції та методи з узагальненими типами!

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

Після цього ви навчитесь використовувати трейти (від англ. <0>trait</0> "властивість, риса"), щоб визначати поведінку в узагальнений спосіб. Ви можете поєднувати трейти з узагальненими типами, щоб обмежити узагальнений тип так, щоб він працював не з будь-якими типами, а лише тими, які мають певну поведінку.

Нарешті ми поговоримо про лайфтайми (від англ. <0>lifetime</0> "час життя"): підвид узагальнених типів, які дають компілятору інформацію про те, як посилання відносяться одне до одного. Лайфтайми дозволяють нам давати компілятору достатньо інформації про позичені значення, щоб він міг впевнитись, що посилання будуть дійсними в тих ситуаціях, де компілятор не знав би цього без наших підказок.

Уникання повторень за допомогою виділення функції

Узагальнені типи дозволяють використати змінну типу, яка замінює багато типів, а не конкретний тип, щоб уникнути повторень у коді. Перед тим як розглянути синтаксис узагальнених типів, погляньмо на уникнення повторень без узагальнених типів, а саме виділення функції, яка замінює конкретні значення на змінну, що представляє багато значень. Тоді ми застосуємо той самий підхід, щоб виділити узагальнену функцію! Поглянувши на те, як помітити продубльований код, який можна винести в окрему функцію, ви почнете помічати продубльований код, який може використовувати узагальнені типи.

We begin with the short program in Listing 10-1 that finds the largest number in a list.

Файл: 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: Finding the largest number in a list of numbers

Ми зберігаємо список цілих чисел у змінній 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);
}

Listing 10-2: Code to find the largest number in two lists of numbers

Хоча цей код працює, дублювання коду виснажливе і збільшує ризик помилок. Також потрібно не забути оновити код у двох місцях, якщо ми хочемо внести будь-які зміни.

Щоб уникнути цього дублювання, ми створимо абстракцію визначивши функцію, що працює з будь-яким списком цілих чисел, переданим як параметр. Це рішення робить наш код більш зрозумілим і дозволяє нам виразити концепцію пошуку найбільшого числа у списку в абстрактний спосіб.

У роздруку 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);
}

Listing 10-3: Abstracted code to find the largest number in two lists

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

In summary, here are the steps we took to change the code from Listing 10-2 to Listing 10-3:

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

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

Наприклад, скажімо, ми маємо дві функції: одна знаходить найбільший елемент у слайсі значень i32, а інша — у слайсі значень char. Як можна уникнути повторень? Давайте дізнаємось!