Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Векторы: списки значений

Первая коллекция, которую мы рассмотрим, — 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), чтобы эта ссылка и все другие ссылки на содержимое вектора оставались действительными. Вспомните правило: в одной области видимости не может быть одновременно изменяемых и неизменяемых ссылок. Это правило срабатывает в листинге 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}");
}
Листинг 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` (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}");
    }
}
Листинг 8-7: Выводим каждый элемент вектора, перебирая элементы циклом for

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

fn main() {
    let mut v = vec![100, 32, 57];
    for i in &mut v {
        *i += 50;
    }
}
Листинг 8-8: Перебираем изменяемые ссылки на элементы вектора

Чтобы изменить значение, на которое указывает изменяемая ссылка, нужно сначала добраться до значения в 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),
    ];
}
Листинг 8-9: Определяем перечисление, чтобы хранить значения разных типов в одном векторе

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

Если заранее неизвестен полный набор типов, которые программа получит во время работы и будет хранить в векторе, приём с перечислением не сработает. Тогда можно использовать трейт-объект — о них мы поговорим в главе 18.

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

Вместе с вектором уничтожаются и его элементы

Как и любая другая структура (struct), вектор освобождается, когда выходит из области видимости, — это показано в листинге 8-10.

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

        // что-то делаем с v
    } // <- здесь v выходит из области видимости, и память освобождается
}
Листинг 8-10: Где уничтожаются вектор и его элементы

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

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