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

Beginner

This tutorial is from open-source community. Access the source code

Введение

Добро пожаловать в Сохранение списков значений с помощью векторов. Этот практикум является частью Rust Book. Вы можете практиковать свои навыки Rust в LabEx.

«В этом практикуме мы изучим коллекцию Vec<T>, также называемую вектором, которая позволяет хранить списки значений одного типа в одной структуре данных».

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

Первым типом коллекции, с которым мы познакомимся, является Vec<T>, также называемый вектором. Векторы позволяют хранить несколько значений в одной структуре данных, которая располагает все значения рядом с друг другом в памяти. Векторы могут хранить только значения одного типа. Они полезны, когда у вас есть список элементов, например, строки текста в файле или цены на товары в корзине покупок.

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

Для создания нового пустого вектора мы вызываем функцию Vec::new, как показано в Листинге 8-1.

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, потому что это стандартный тип целого числа, как мы обсуждали в разделе "Типы данных".

let v = vec![1, 2, 3];

Листинг 8-2: Создание нового вектора, содержащего значения

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

Обновление вектора

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

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.

let v = vec![1, 2, 3, 4, 5];

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

2 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, чтобы получить третий элемент [1], потому что векторы индексируются числами, начиная с нуля. Использование & и [] дает нам ссылку на элемент с указанным индексным значением. Когда мы используем метод get с переданным в качестве аргумента индексом [2], мы получаем Option<&T>, которое мы можем использовать с match.

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

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, вы можете сообщить пользователю, сколько элементов в текущем векторе, и дать им еще один шанс ввести правильное значение. Это будет более дружелюбным по отношению к пользователю, чем завершение программы из-за опечатки!

Когда программа имеет допустимую ссылку, проверщик ссылок накладывает правила владения и заимствования (рассмотренные в главе 4), чтобы убедиться, что эта ссылка и любые другие ссылки на содержимое вектора остаются валидными. Назовем правило, по которому нельзя иметь изменяемую и неизменяемую ссылки в одной области видимости. Это правило применяется в Листинге 8-6, где у нас есть неизменяемая ссылка на первый элемент в векторе, и мы пытаемся добавить элемент в конец. Эта программа не будет работать, если мы также попытаемся обратиться к этому элементу позже в функции.

let mut v = vec![1, 2, 3, 4, 5];

let first = &v[0];

v.push(6);

println!("The first element is: {first}");

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

Компиляция этого кода приведет к следующей ошибке:

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

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

Примечание: Для получения дополнительной информации об имплементационных деталях типа Vec<T> см. "Rustonomicon" по адресу https://doc.rust-lang.org/nomicon/vec/vec.html.

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

Чтобы依次访问向量中的每个元素,我们需要遍历所有元素,而不是使用索引逐个访问。清单 8-7 展示了如何使用for循环获取i32值向量中每个元素的不可变引用并打印它们。

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

清单 8-7:通过使用for循环遍历元素来打印向量中的每个元素

我们还可以遍历可变向量中每个元素的可变引用,以便对所有元素进行更改。清单 8-8 中的for循环将为每个元素加上50

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

清单 8-8:遍历向量中元素的可变引用

为了更改可变引用所指向的值,我们必须使用*解引用运算符来获取i中的值,然后才能使用+=运算符。我们将在“跟随指针获取值”中更多地讨论解引用运算符。

由于借用检查器的规则,无论以不可变还是可变方式遍历向量都是安全的。如果我们试图在清单 8-7 和清单 8-8 的for循环体中插入或删除项目,我们将得到一个类似于清单 8-6 中的代码所得到的编译器错误。for循环所持有对向量的引用可防止同时修改整个向量。

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

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

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

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

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

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

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

Уничтожение вектора уничтожает его элементы

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

{
    let v = vec![1, 2, 3, 4];

    // do stuff with v
} // <- v выходит из области видимости и освобождается здесь

Листинг 8-10: Показывает, где вектор и его элементы уничтожаются

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

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

Резюме

Поздравляем! Вы завершили лабораторную работу по хранению списков значений с использованием векторов. Вы можете выполнить больше лабораторных работ в LabEx, чтобы улучшить свои навыки.