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();
}
Listing 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];
}
Listing 8-2: Создание нового вектора, содержащего значения

Поскольку мы предоставили начальные значения i32, Rust может вывести, что тип vVec<i32>, и аннотация типа не требуется. Далее мы посмотрим, как изменять вектор.

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

Чтобы создать вектор, а затем добавить в него элементы, мы можем использовать метод push, как показано в Листинге 8-3.

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

    v.push(5);
    v.push(6);
    v.push(7);
    v.push(8);
}
Listing 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."),
    }
}
Listing 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);
}
Listing 8-5: Попытка доступа к элементу с индексом 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}");
}
Listing 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}");
    }
}
Listing 8-7: Печать каждого элемента в векторе путём итерации по элементам с использованием цикла for

Чтобы прочитать число, на которое указывает n_ref, мы должны использовать оператор разыменования *, чтобы получить значение в n_ref, прежде чем мы сможем прибавить 1 к нему, как рассматривалось в “Разыменование указателя даёт доступ к его данным”.

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

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

Чтобы изменить значение, на которое указывает изменяемая ссылка, мы снова используем оператор разыменования *, чтобы получить значение в n_ref, прежде чем сможем использовать оператор +=.

Безопасное использование итераторов

Мы обсудим подробнее, как работают итераторы, в Главе 13.2 “Обработка серии элементов с помощью итераторов”. Пока что одна важная деталь: итераторы содержат указатель на данные внутри вектора. Мы можем увидеть, как работают итераторы, раскрыв цикл for в соответствующие вызовы методов Vec::iter и Iterator::next:

Заметьте, что итератор iter — это указатель, который перемещается через каждый элемент вектора. Метод next продвигает итератор и возвращает необязательную ссылку на предыдущий элемент, либо Some (которую мы раскрываем), либо None в конце вектора.

Эта деталь важна для безопасного использования векторов. Например, предположим, мы хотим продублировать вектор на месте, так что [1, 2] станет [1, 2, 1, 2]. Наивная реализация может выглядеть так, с аннотациями разрешений, выведенных компилятором:

Заметьте, что v.iter() удаляет разрешение W из *v. Поэтому операция v.push(..) не имеет ожидаемого разрешения W. Компилятор Rust отклонит эту программу с соответствующим сообщением об ошибке:

error[E0502]: cannot borrow `*v` as mutable because it is also borrowed as immutable
 --> test.rs:3:9
  |
2 |     for n_ref in v.iter() {
  |                  --------
  |                  |
  |                  immutable borrow occurs here
  |                  immutable borrow later used here
3 |         v.push(*n_ref);
  |         ^^^^^^^^^^^^^^ mutable borrow occurs here

Как мы обсуждали в Главе 4, проблема безопасности, лежащая в основе этой ошибки, — это чтение освобождённой памяти. Как только происходит v.push(1), вектор перераспределит своё содержимое и сделает указатель итератора недействительным. Поэтому для безопасного использования итераторов Rust не позволяет добавлять или удалять элементы из вектора во время итерации.

Один способ итерировать по вектору без использования указателя — использовать диапазон, как мы использовали для срезов строк в Главе 4.4. Например, диапазон 0 .. v.len() — это итератор по всем индексам вектора v, как видно здесь:

Использование перечисления для хранения нескольких типов

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

Например, предположим, мы хотим получить значения из строки в электронной таблице, в которой некоторые столбцы в строке содержат целые числа, некоторые числа с плавающей точкой, а некоторые строки. Мы можем определить перечисление, варианты которого будут хранить разные типы значений, и все варианты перечисления будут считаться одним типом: типом перечисления. Затем мы можем создать вектор, который будет хранить это перечисление, а значит, в конечном итоге, хранить разные типы. Мы продемонстрировали это в Листинге 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),
    ];
}
Listing 8-9: Определение enum для хранения значений разных типов в одном векторе

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
}
Listing 8-10: Показ места, где вектор и его элементы удаляются

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

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