Зберігання списків значень за допомогою векторів
Першим типом колекції, який ми розглянемо, є Vec<T>, також відомий як вектор.
Вектори дозволяють вам зберігати більше ніж одне значення в одній структурі даних, яка
розміщує всі значення поруч одне з одним у пам’яті. Вектори можуть зберігати лише значення
одного й того самого типу. Вони корисні, коли у вас є список елементів, таких як
рядки тексту у файлі або ціни елементів у кошику покупок.
Створення нового вектора
Щоб створити новий, порожній вектор, ми викликаємо функцію Vec::new, як показано в
Лістингу 8-1.
fn main() {
let v: Vec<i32> = Vec::new();
}
Зверніть увагу, що ми додали тут анотацію типу. Оскільки ми не вставляємо жодних
значень у цей вектор, 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];
}
Оскільки ми надали початкові значення 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);
}
Як і для будь-якої змінної, якщо ми хочемо мати змогу змінювати її значення, нам потрібно
зробити її змінною за допомогою ключового слова 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."),
}
}
Зверніть увагу на кілька деталей тут. Ми використовуємо значення індексу 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);
}
Коли ми запускаємо цей код, перший метод [] призведе до паніки програми,
тому що він посилається на неіснуючий елемент. Цей метод найкраще використовувати, коли ви
хочете, щоб ваша програма аварійно завершувалася, якщо є спроба отримати доступ до елемента за межами
кінця вектора.
Коли методу get передається індекс, що лежить поза межами вектора, він повертає
None без паніки. Ви б використовували цей метод, якщо доступ до елемента
поза межами вектора може інколи траплятися за нормальних
обставин. Тоді ваш код матиме логіку для обробки або
Some(&element), або None, як обговорювалося в Розділі 6. Наприклад, індекс
може надходити від людини, яка вводить число. Якщо вона випадково введе
занадто велике число, і програма отримає значення None, ви могли б повідомити
користувачу, скільки елементів є в поточному векторі, і дати йому ще одну спробу
ввести коректне значення. Це було б дружніше до користувача, ніж аварійне завершення програми
через друкарську помилку!
Коли програма має дійсне посилання, перевірник запозичень забезпечує правила володіння та запозичення (розглянуті в Розділі 4), щоб гарантувати, що це посилання та будь-які інші посилання на вміст вектора залишаються дійсними. Пригадайте правило, яке стверджує, що ви не можете мати змінні та незмінні посилання в одній і тій самій області видимості. Це правило застосовується в Лістингу 8-6, де ми тримаємо незмінне посилання на перший елемент у векторі й намагаємося додати елемент до кінця. Ця програма не працюватиме, якщо ми також спробуємо послатися на цей елемент пізніше у функції.
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {first}");
}
Компіляція цього коду призведе до такої помилки:
$ 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` (bin "collections") due to 1 previous error
Код у Лістингу 8-6 може виглядати так, ніби він мав би працювати: чому посилання на перший елемент має зважати на зміни в кінці вектора? Ця помилка зумовлена тим, як працюють вектори: оскільки вектори розміщують значення поруч одне з одним у пам’яті, додавання нового елемента в кінець вектора може вимагати виділення нової пам’яті та копіювання старих елементів у новий простір, якщо немає достатньо місця, щоб розмістити всі елементи поруч один з одним там, де вектор зараз зберігається. У такому разі посилання на перший елемент буде вказувати на звільнену пам’ять. Правила запозичення запобігають тому, щоб програми опинялися в такій ситуації.
Примітка: Щоб дізнатися більше про деталі реалізації типу
Vec<T>, дивіться “The Rustonomicon”.
Ітерування по значеннях у векторі
Щоб послідовно отримати доступ до кожного елемента у векторі, ми ітерували б по всіх
елементах замість використання індексів для доступу до них по одному. Лістинг 8-7 показує, як
використати цикл for, щоб отримати незмінні посилання на кожен елемент у векторі
значень i32 і вивести їх.
fn main() {
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
}
Ми також можемо ітерувати по змінних посиланнях на кожен елемент у змінному векторі,
щоби вносити зміни до всіх елементів. Цикл for у Лістингу 8-8
додасть 50 до кожного елемента.
fn main() {
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
}
Щоб змінити значення, на яке вказує змінне посилання, ми маємо використати
оператор розіменування *, щоб дістатися до значення в i, перш ніж ми зможемо використати
оператор +=. Ми ще поговоримо більше про оператор розіменування в розділі “Перехід за посиланням до значення” Розділу 15.
Ітерація по вектору, чи то незмінно, чи то змінно, є безпечною завдяки
правилам перевірника запозичень. Якщо б ми спробували вставити або видалити елементи в тілах
циклу 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),
];
}
Rust потрібно знати, які типи будуть у векторі під час компіляції, щоб він
знав точно, скільки пам’яті на купі буде потрібно для зберігання кожного елемента.
Ми також маємо явно вказати, які типи дозволені в цьому векторі. Якби Rust
дозволив вектору містити будь-який тип, існувала б імовірність, що один або більше
типів спричинять помилки з операціями, виконаними над елементами
вектора. Використання перелічення разом із виразом match означає, що Rust забезпечить
під час компіляції, щоб було оброблено кожен можливий випадок, як обговорювалося в Розділі 6.
Якщо ви не знаєте повного набору типів, які програма отримає під час виконання, щоб зберегти їх у векторі, техніка з переліченням не працюватиме. Натомість ви можете використати об’єкт трейту, який ми розглянемо в Розділі 18.
Тепер, коли ми обговорили деякі з найпоширеніших способів використання векторів, обов’язково
перегляньте документацію 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
}
Коли вектор звільняється, весь його вміст також звільняється, тобто цілі числа, які він містить, будуть очищені. Перевірник запозичень гарантує, що будь-які посилання на вміст вектора використовуються лише тоді, коли сам вектор є дійсним.
Перейдемо до наступного типу колекції: String!