Узагальнені Типи, Трейти та Часи Існування

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

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

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

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

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

Уникнення Повторень Шляхом Відокремлення Коду у Функцію

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

Почнімо з короткої програми у роздруку 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. Як можна уникнути повторень? Нумо дізнаймося!