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

Первым типом коллекции, который мы разберём, будет 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!("Третий элемент: {third}");

    let third: Option<&i32> = v.get(2);
    match third {
        Some(third) => println!("Третий элемент: {third}"),
        None => println!("Третьего элемента не содержится."),
    }
}

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

Rust предоставляет эти два способа ссылки на элемент, чтобы вы могли выбирать поведение программы при попытке использовать значение индекса за пределами диапазона существующих элементов. В качестве примера, давайте посмотрим, как поведут себя оба способа, если мы будем пытаться получить доступ к элементу с индексом 100 из вектора из пяти элементов:

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!("Первый элемент: {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!("Первый элемент: {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("синий")),
        SpreadsheetCell::Float(10.12),
    ];
}

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

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

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

Высвобождение вектора высвобождает его элементы

Подобно структурам, вектор высвобождает свою память, когда выходит из области видимости, как показано в Листинге 8-10.

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

        // какая-нибудь работа с v
    } // <- здесь v покидает область видимости и высвобождается
}

Когда вектор высвобождается, всё его содержимое также высвобождается, то есть все числа, хранившиеся в векторе из примера выше, удаляются. Анализатор заимствований гарантирует, что любые ссылки на содержимое вектора используются только тогда, когда сам вектор действителен.

Перейдём к следующей коллекции: String.