Сохранение списка значений с помощью вектора

Первым типом коллекции, который мы разберём, будет Vec, также известный как вектор (vector). Векторы позволяют сохранять более одного значения в одной структуре данных, сохраняющей элементы в памяти один за другим. Векторы могут сохранять данные только одного типа. Их удобно использовать, когда нужно сохранить список элементов, например, список текстовых строк в файле, или список цен товаров из корзины покупок.

Создание нового вектора

Для создания нового вектора используется функция 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.

В более реальном коде, Rust часто может вывести тип сохраняемых вами значений, как только вы вставите значения в вектор. Так что вам довольно редко нужна данная аннотация типа. Более общим является создание Vec<T> имеющего начальные значения: для удобства 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 и аннотация типа здесь не нужна. Далее мы посмотрим как изменять вектор.

Изменение вектора

Чтобы создать вектор и затем добавить к нему элементы, можно использовать метод 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>.

Удаление элементов из вектора

Подобно структурам struct, вектор высвобождает свою память когда выходит из области видимости функции в которой он определён, данное поведение прокомментировано в листинге 8-4.

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

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

Листинг 8-4. Показывает, где вектор и его элементы уже будут удалены.

Когда вектор удаляется, всё его содержимое также удаляется: удаление вектора означает и удаление значений, которые он содержит. Это может показаться простой концепцией, но все становится немного сложнее, когда вы начинаете вводить ссылки на элементы вектора. Давайте займёмся этим далее!

Чтение данных вектора

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

В листинге 8-5 показаны оба метода доступа к значению в векторе: либо с помощью синтаксиса индексации, либо с помощью метода get.

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

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

    match v.get(2) {
        Some(third) => println!("The third element is {}", third),
        None => println!("There is no third element."),
    }
}

Листинг 8-5. Использование синтаксиса индексации или метода get для доступа к элементу в векторе

Обратите внимание здесь на пару деталей. Во-первых, используется значение индекса 2 для получения третьего элемента: векторы индексируются начиная с нуля. Во-вторых, есть два способа получения третьего элемента: либо используя & с [] возвращающих ссылку на элемент, либо с помощью метода get содержащего индекс, переданный в качестве аргумента, который возвращает Option<&T>.

В Rust есть два способа ссылаться на элемент, поэтому вы можете выбрать, как будет вести себя программа, когда вы попытаетесь использовать значение индекса, для которого в векторе нет элемента. В качестве примера давайте посмотрим, что будет делать программа, если в ней определён вектор, содержащий пять элементов, но она пытается получить доступ к элементу с индексом 100, как показано в листинге 8-6.

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

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

Листинг 8-6. Попытка доступа к элементу с индексом 100 в векторе, содержащем всего пять элементов

Когда мы запускаем этот код, первая строка с &v[100] вызовет панику программы, потому что происходит попытка получить ссылку на несуществующий элемент. Такой подход лучше всего использовать, когда вы хотите, чтобы ваша программа аварийно завершила работу при попытке доступа к элементу за пределами вектора.

Когда методу get передаётся индекс, который находится за пределами вектора, он без паники возвращает None. Вы могли бы использовать такой подход, если доступ к элементу за пределами диапазона вектора происходит время от времени при нормальных обстоятельствах. Тогда ваш код будет иметь логику для обработки наличия Some(&element) или None, как обсуждалось в Главе 6. Например, индекс может исходить от человека, вводящего число. Если пользователь случайно введёт слишком большое число, то программа получит значение None и у вас будет возможность сообщить пользователю, сколько элементов находится в текущем векторе, и дать ему ещё один шанс ввести допустимое значение. Такое поведение было бы более дружелюбным для пользователя, чем внезапный сбой программы из-за опечатки!

Когда у программы есть действительная ссылка, borrow checker (средство проверки заимствований), обеспечивает соблюдение правил владения и заимствования (описаны в Главе 4), чтобы гарантировать, что эта ссылка и любые другие ссылки на содержимое вектора остаются действительными. Вспомните правило, которое гласит, что у вас не может быть изменяемых и неизменяемых ссылок в одной и той же области. Это правило применяется в листинге 8-7, где мы храним неизменяемую ссылку на первый элемент вектора и затем пытаемся добавить элемент в конец вектора, что не сработает:

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-7. Попытка добавить некоторый элемент в вектор, в то время когда есть ссылка на элемент вектора

Компиляция этого кода приведёт к ошибке:

$ 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

error: aborting due to previous error

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

To learn more, run the command again with --verbose.

Код в Листинге 8-7 может выглядеть вполне рабочим: почему ссылка на первый элемент должна беспокоится про изменения в конце вектора? Ошибка, о которой сообщает компилятор, связана с тем, как работают векторы. Добавление нового элемента в конец вектора, может потребовать выделение нового участка памяти и копирования старых элементов в него. Повторное выделение памяти произойдёт если там, где вектор находится в настоящее время, недостаточно места для размещения новых элементов рядом со старыми - придётся разместить вектор в новом месте. В этом случае ссылка на первый элемент будет указывать на освобождённую память. Правила заимствования не допускают, чтобы программа оказалась в такой ситуации.

Примечание: Дополнительные сведения о реализации типа Vec<T> смотрите в разделе "The Rustonomicon".

Перебор значений в векторе

Если мы хотим получить доступ к каждому элементу вектора по очереди, мы можем перебирать все элементы, а не использовать индексы для доступа по одному за раз. В листинге 8-8 показано, как использовать цикл for для получения неизменяемых ссылок на каждый элемент в векторе со значениями типа i32 и их печати.

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

Листинг 8-8. Печать каждого элемента вектора, при помощи итерирования по элементам вектора с помощью цикла for

Мы также можем итерировать изменяемые ссылки на каждый элемент изменяемого вектора, чтобы вносить изменения во все элементы. Цикл for в листинге 8-9 добавит 50 к каждому элементу.

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

Листинг 8-9. Итерирование и изменение элементов вектора по изменяемым ссылкам

Чтобы изменить значение на которое ссылается изменяемая ссылка, мы должны использовать оператор разыменования ссылки (*) для получения значения по ссылке в переменной i прежде чем использовать оператор +=. Мы поговорим подробнее об операторе разыменования в разделе "Следуя указателю на значение с помощью оператора разыменования" Главы 15.

Использование перечислений для хранения множества разных типов

В начале этой главы мы говорили, что векторы могут хранить только значения одинакового типа. Это может быть неудобно; безусловно есть случаи, когда необходим список из элементов разного типа. В этом нам смогут помочь перечисления. К счастью, варианты перечисления определены в самом типе перечисления, поэтому когда нам нужно хранить в векторе элементы различного типа, можно определить и использовать перечисление!

Например, мы хотим получить значения из строки в электронной таблице где некоторые столбцы строки содержат целые числа, некоторые числа с плавающей точкой, а другие - вообще строковые значения. Можно определить перечисление, варианты которого будут содержать разные типы значений и тогда все варианты перечисления будут считаться одними и тем же типом: типом самого перечисления. Затем можно создать вектор, который содержит в себе значения этого перечисления и таким образом в конечном счёте добиться того, что в векторе будут сохраняться значения разного типа. Мы продемонстрировали это в листинге 8-10.

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-10. Определение enum для хранения значений разных типов в векторе

Rust должен знать, какие типы будут в векторе во время компиляции, чтобы знать сколько именно памяти в куче потребуется для хранения каждого элемента. Вторичным преимуществом является то, что мы можем чётко указать какие типы разрешены в таком векторе. Если бы Rust позволял вектору содержать любой тип, был бы шанс что один или несколько типов будут вызывать ошибки при выполнении операций над элементами вектора. Использование перечисления вместе с выражением match означает, что Rust сможет убедиться во время компиляции что обработаны все возможные случаи, как обсуждалось в Главе 6.

При написании программы, если вы не знаете исчерпывающий набор типов, которые программа может получить во время своего выполнения, которые потребуется сохранить в векторе, то техника использования перечисления не будет работать. Вместо этого вы можете использовать объект типажа, trait object, который мы рассмотрим в Главе 17.

Теперь, когда мы обсудили некоторые из наиболее распространённых способов использования векторов, просмотрите документацию по API для знакомства со всем множеством полезных методов, определённых для Vec<T> в стандартной библиотеке. Например, в дополнение к методу push, существует метод pop который одновременно удалит и вернёт последний элемент вектора. Давайте перейдём к следующему типу коллекций: строкам String!