Зберігання Списків Значень з Векторами

Перший тип колекцій, який ми розглянемо - це Vec<T>, також відомий як вектор. Вектори дозволять вам зберігати більше одного значення в єдиній структурі даних, що розташовує ці значення поруч один з одним у пам'яті. Вектор може зберігати лише значення одного типу. Вони корисні, коли ви маєте список предметів, наприклад рядки тексту у файлі або ціни на товари у кошику.

Створення Нового Вектора

Щоб створити новий порожній вектор, ми викликаємо Vec:new, як показано в Блоці коду 8-1.

fn main() {
    let v: Vec<i32> = Vec::new();
}

Блок коду 8-1: створення нового порожнього вектора для зберігання значень типу i32

Зауважте, що тут ми додали анотації типу. Оскільки ми не вставляємо жодного значення в цей вектор, Rust не знає, які елементи ми маємо намір зберігати. Це важлива деталь. Вектори реалізовані за допомогою узагальнень; ми розкажемо, як використовувати узагальнення з вашими власними типами в Розділі 10. Наразі треба знати лише, що тип Vec<T>, наданий стандартною бібліотекою, може містити будь-який тип. Коли ми створюємо вектор, що міститиме певний тип, ми можемо зазначити тип у кутових дужках. У Блоці коду 8-1 ми кажемо Rust, що Vec<T> у v міститиме елементи типу i32.

Зазвичай ви створюватимете Vec<T> з початковими значеннями, і Rust виведе тип значень, які ви хочете зберігати, тож вам нечасто буде потрібно додавати таку анотацію типу. Rust для зручності надає макрос vec!, який створює новий вектор, який містить ваші значення. Блок коду 8-2 створює новий Vec<i32>, що містить значення 1, 2, і 3. Тип цілих - i32, бо це тип цілих за замовчуванням, як ми вже говорили в підрозділі “Типи даних” Розділу 3.

fn main() {
    let v = vec![1, 2, 3];
}

Блок коду 8-2: створення нового вектора, що містить значення

Оскільки ми надали початкові значення i32, Rust може вивести, що типом v є Vec<i32> і анотація типу тут не потрібна. Далі ми поглянемо, як змінити вектор.

Оновлення Вектора

Щоб створити вектор і додати до нього елементи ми можемо використати метод push, як показано в Блоці коду 8-3.

fn main() {
    let mut v = Vec::new();

    v.push(5);
    v.push(6);
    v.push(7);
    v.push(8);
}

Блок коду 8-3: використання методу push для додавання значень у вектор

Як і для будь-якої змінної, якщо ми хочемо змінювати її значення, ми повинні зробити його мутабельним за допомогою ключового слова mut, як говорилося в Розділі 3. Числа, як ми розміщуємо у векторі, мають тип i32, і Rust виводить це з даних, тож нам не потрібна анотація Vec<i32>.

Читання Елементів Векторів

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

Блок коду 8-4 показує обидва методи доступу до значення у векторі - синтаксис індексування і метод get.

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let third: &i32 = &v[2];
    println!("The third element is {third}");

    let third: Option<&i32> = v.get(2);
    match third {
        Some(third) => println!("The third element is {third}"),
        None => println!("There is no third element."),
    }
}

Блок коду 8-4: використання синтаксису індексів або методу get для доступу до елементів вектора

Зверніть тут увагу на декілька деталей. Ми використовуємо значення індексу 2, щоб отримати третій елемент, бо вектори індексуються числами, починаючи з нуля. Використання & і [] надає нам посилання на елемент за значенням індексу. Коли ми використовуємо метод get з індексом, переданим аргументом, то отримуємо Option<&T>, який ми можемо використати у match.

Rust надає ці два способи посилання на елемент, щоб ви могли вибрати, як програма поводиться при спробі використовувати значення індексу поза діапазоном наявних елементів. Як приклад, подивімося, що станеться, коли ми матємо вектор з п'яти елементів, а потім спробуємо отримати доступ до елемента з індексом 100 за допомогою кожної техніки, як показано в Блоці коду 8-5.

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let does_not_exist = &v[100];
    let does_not_exist = v.get(100);
}

Блок коду 8-5: спроба доступу до елементу з індексом 100 у векторі, що містить п'ять елементів

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

Коли методу get передається індекс, що знаходиться поза вектором, він повертає None без паніки. Цей метод краще використовувати, якщо доступ до елемента за межами вектора може ставатися час від часу за нормальних умов. Ваш код тоді міститиме логіку обробки як Some(&element), так і None, як пояснюється в Розділі 6. Наприклад, індекс може бути отримано від людини, що вводить число. Якщо хтось випадково введе завелике число і програма отримає значення None, чи можете повідомити користувачеві, скільки елементів є у векторі надати йому ще одну спробу ввести коректне значення. Це буде більш дружньо до користувача, ніж аварійне завершення програми через хибодрук!

Коли у програми є посилання, borrow checker забезпечує правила володіння і позичання (про які йдеться у Розділі 4), забезпечуючи, що це посилання та будь-які інші посилання на вміст вектора залишаються коректними. Згадайте правило, яке каже, що не можна мати мутабельні і немутабельні посилання в одній області видимості. Це правило застосовується в Блоці коду 806, де ми тримаємо немутабельне посилання на перший елемент вектора і намагаємося додати елемент у кінець. Ця програма не спрацює, якщо ми спробуємо звернутися до цього елемента пізніше у функції:

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];

    let first = &v[0];

    v.push(6);

    println!("The first element is: {first}");
}

Блок коду 8-6: спроба додати елемент до вектора, тримаючи посилання на його елемент

Компіляція цього коду завершиться з такою помилкою:

$ cargo run
   Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:5
  |
4 |     let first = &v[0];
  |                  - immutable borrow occurs here
5 |
6 |     v.push(6);
  |     ^^^^^^^^^ mutable borrow occurs here
7 |
8 |     println!("The first element is: {first}");
  |                                      ----- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` due to previous error

Код у Блоці коду 8-6, можливо, має вигляд, ніби він повинен працювати: чому посилання на перший елемент має турбуватися про зміни в кінці вектора? Ця помилка відбувається через те, як працюють вектори: оскільки вектори тримають значення поруч одне з одним у пам'яті, додавання нового елемента в кінець вектора може вимагати виділення нової пам'яті та копіювання старих елементів у нове місце, якщо там, де наразі зберігається вектор, недостатньо місця, щоб тримати всі елементи один біля одного. У такому разі посилання на перший елемент вказуватиме на звільнену пам'ять. Правила позичання перешкоджають програмі опинитися в такій ситуації.

Примітка: Для деталей імплементації типу Vec<T> дивіться "Растономікон".

Ітерування над Значеннями Вектора

Для доступу до кожного елемента вектора по черзі ми ітеруємо по всіх елементах замість використання індексів для доступу по одному за раз. Блок коду 8-7 показує, як використовувати цикл for, щоб отримати немутабельні посилання на кожен елемент вектора значень i32 і вивести їх.

fn main() {
    let v = vec![100, 32, 57];
    for i in &v {
        println!("{i}");
    }
}

Блок коду 8-7: виведення кожного елементу вектора ітеруванням по елементах у циклі for

Ми також можемо ітерувати по мутабельних посиланнях на кожен елемент у мутабельному векторі, щоб змінити всі елементи. Цикл для у Блоці коду 8-8 додасть 50 до кожного елемента.

fn main() {
    let mut v = vec![100, 32, 57];
    for i in &mut v {
        *i += 50;
    }
}

Блок коду 8-8: Ітерування по мутабельних посиланнях на елементи у векторі

Щоб змінити значення, на яке посилається мутабельне посилання, нам потрібно скористатися оператором розіменування * для отримання значення в і до того, як ми зможемо використовувати оператор +=. Ми поговоримо більше про оператора розіменування у підрозділі "Перехід за вказівником до значення" Розділу 15.

Ітерування по вектору, мутабельне чи немутабельне, є безпечним завдяки правилам borrow checker. Якби ми спробували вставити або видалити елементи в циклі for у Блоці коду 8-7 і Блоці коду 8-8, то отримали б помилку компілятора, схожу на той, що ми отримали з кодом у Блоці коду 8-6. Посилання на вектор, яке тримає цикл for, запобігає одночасній зміні усього вектора.

Використання Енума для Зберігання Декількох Типів

Вектор може зберігати лише значення одного типу. Це може бути незручно; точно існують випадки, коли є потреба у зберіганні списку елементів різних типів. На щастя, варіанти енума визначені як один тип, тож коли нам потрібен один тип для представлення елементів різних типів, ми можемо визначити і використовувати енум!

Наприклад, нехай ми хочемо отримати значення з рядка в таблиці, у якій деякі стовпці в рядку містять цілі числа, деякі — числа з рухомими точками, а деякі — рядки. Ми можемо визначити енум, варіанти якого будуть містити різні типи значень, і всі варіанти енума будуть вважатися одним і тим же типом — енумом. Тоді ми можемо створити вектор, який міститиме цей енум і, зрештою, міститиме різні типи. Ми продемонстрували це у Блоці коду 8-9.

fn main() {
    enum SpreadsheetCell {
        Int(i32),
        Float(f64),
        Text(String),
    }

    let row = vec![
        SpreadsheetCell::Int(3),
        SpreadsheetCell::Text(String::from("blue")),
        SpreadsheetCell::Float(10.12),
    ];
}

Блок коду 8-9: Визначення enum для зберігання значень різних типів у одному векторі

Rust має знати, які типи будуть у векторі, під час компіляції, щоб знати, скільки саме пам'яті у купі буде потрібно для зберігання кожного елемента. Ми також маємо явно зазначити, які типи можуть бути в цьому векторі. Якби Rust дозволив вектору містити будь-який тип, була б імовірність, що один або кілька з типів призведуть до помилок при виконанні операцій на елементах вектора. Використання енуму і виразу match означає, що Rust гарантує під час компіляції, що умі можливі випадки буде оброблено, як обговорювалося в Розділі 6.

Якщо ви не маєте вичерпного списку типів, з якими програма працюватиме під час виконання для зберігання у векторі, техніка енумів не спрацює. Натомість ви можете скористатися трейтовими об'єктами, про які йдеться у Розділі 17.

Тепер, коли ми обговорили деякі найпоширеніші способи використання векторів, обов'язково подивитися документацію API щоб дізнатися про багато інших корисних методів, визначених для Vec<T> у стандартній бібліотеці. Наприклад, на додачу до методу push, метод pop видаляє і повертає останній елемент.

Очищення Вектора Очищує Його Елементи

Як і будь-яка інша struct, вектор вивільняється, коли виходить з області видимості, як підписано в Блоці коду 8-10.

fn main() {
    {
        let v = vec![1, 2, 3, 4];

        // do stuff with v
    } // <- v goes out of scope and is freed here
}

Блок коду 8-10: демонстрація, де саме вектор і його елементи очищуються

Коли вектор очищуються, також очищується і його вміст, тобто цілі числа, які він містить, будуть очищені. Borrow checker гарантує, що будь-які посилання на вміст вектора використовуються лише поки сам вектор є коректним.

Перейдімо до наступного типу колекцій: String!