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

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

The first collection type we’ll look at is Vec<T>, also known as a vector. Vectors allow you to store more than one value in a single data structure that puts all the values next to each other in memory. Vectors can only store values of the same type. They are useful when you have a list of items, such as the lines of text in a file or the prices of items in a shopping cart.

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

To create a new, empty vector, we call the Vec::new function, as shown in Listing 8-1.

fn main() {
    let v: Vec<i32> = Vec::new();
}
Listing 8-1: Creating a new, empty vector to hold values of type i32

Обратите внимание, что здесь мы добавили аннотацию типа. Поскольку мы не помещаем никаких значений в этот вектор, Rust не знает, элементы какого типа мы собираемся хранить. Это важный момент. Векторы реализованы с использованием обобщённых типов; мы рассмотрим, как использовать обобщённые типы с вашими собственными типами, в Главе 10. А пока знайте, что тип Vec<T>, предоставляемый стандартной библиотекой, может хранить любой тип. Когда мы создаём новый вектор для хранения конкретного типа, мы можем указать этот тип в угловых скобках. В Листинге 8-1 мы сообщили Rust, что Vec<T> в v будет хранить элементы типа i32.

More often, you’ll create a Vec<T> with initial values, and Rust will infer the type of value you want to store, so you rarely need to do this type annotation. Rust conveniently provides the vec! macro, which will create a new vector that holds the values you give it. Listing 8-2 creates a new Vec<i32> that holds the values 1, 2, and 3. The integer type is i32 because that’s the default integer type, as we discussed in the “Data Types” section of Chapter 3.

fn main() {
    let v = vec![1, 2, 3];
}
Listing 8-2: Creating a new vector containing values

Поскольку мы указали начальные значения типа 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);
}
Listing 8-3: Using the push method to add values to a vector

Как и с любой переменной, если мы хотим изменить её значение, нам нужно сделать её изменяемой с помощью ключевого слова 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!("Третьего элемента не содержится."),
    }
}
Listing 8-4: Using indexing syntax and using the get method to access an item in a vector

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

Rust provides these two ways to reference an element so that you can choose how the program behaves when you try to use an index value outside the range of existing elements. As an example, let’s see what happens when we have a vector of five elements and then we try to access an element at index 100 with each technique, as shown in Listing 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: Attempting to access the element at index 100 in a vector containing five elements

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

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

When the program has a valid reference, the borrow checker enforces the ownership and borrowing rules (covered in Chapter 4) to ensure that this reference and any other references to the contents of the vector remain valid. Recall the rule that states you can’t have mutable and immutable references in the same scope. That rule applies in Listing 8-6, where we hold an immutable reference to the first element in a vector and try to add an element to the end. This program won’t work if we also try to refer to that element later in the function.

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

    let first = &v[0];

    v.push(6);

    println!("Первый элемент: {first}");
}
Listing 8-6: Attempting to add an element to a vector while holding a reference to an item

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

$ 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

The code in Listing 8-6 might look like it should work: Why should a reference to the first element care about changes at the end of the vector? This error is due to the way vectors work: Because vectors put the values next to each other in memory, adding a new element onto the end of the vector might require allocating new memory and copying the old elements to the new space, if there isn’t enough room to put all the elements next to each other where the vector is currently stored. In that case, the reference to the first element would be pointing to deallocated memory. The borrowing rules prevent programs from ending up in that situation.

Примечание: Дополнительные сведения о реализации типа 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: Printing each element in a vector by iterating over the elements using a for loop

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

fn main() {
    let mut v = vec![100, 32, 57];
    for i in &mut v {
        *i += 50;
    }
}
Listing 8-8: Iterating over mutable references to elements in a vector

To change the value that the mutable reference refers to, we have to use the * dereference operator to get to the value in i before we can use the += operator. We’ll talk more about the dereference operator in the “Following the Reference to the Value” section of Chapter 15.

Итерирование по вектору, будь то неизменяемому или изменяемому, безопасно из-за правил проверки заимствований. Если бы мы попытались вставить или удалить элементы (в/из вектора) в телах цикла for в Листингах 8-7 и 8-8, мы бы получили ошибку компиляции, подобную той, которую мы получили с кодом Листинга 8-6. Ссылка на вектор, перебираемый циклом for, предотвращает одновременную модификацию всего вектора.

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

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

For example, say we want to get values from a row in a spreadsheet in which some of the columns in the row contain integers, some floating-point numbers, and some strings. We can define an enum whose variants will hold the different value types, and all the enum variants will be considered the same type: that of the enum. Then, we can create a vector to hold that enum and so, ultimately, hold different types. We’ve demonstrated this in Listing 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),
    ];
}
Listing 8-9: Defining an enum to store values of different types in one vector

Rust needs to know what types will be in the vector at compile time so that it knows exactly how much memory on the heap will be needed to store each element. We must also be explicit about what types are allowed in this vector. If Rust allowed a vector to hold any type, there would be a chance that one or more of the types would cause errors with the operations performed on the elements of the vector. Using an enum plus a match expression means that Rust will ensure at compile time that every possible case is handled, as discussed in Chapter 6.

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

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

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

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

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

        // какая-нибудь работа с v
    } // <- здесь v покидает область видимости и высвобождается
}
Listing 8-10: Showing where the vector and its elements are dropped

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

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