Хранение списков значений с помощью векторов
Первый тип коллекции, который мы рассмотрим, – это Vec<T>, также известный
как вектор. Векторы позволяют хранить более одного значения в одной структуре
данных, размещая все значения рядом друг с другом в памяти. Векторы могут
хранить значения только одного и того же типа. Они полезны, когда у вас есть
список элементов, например строки текста в файле или цены товаров в корзине
покупок.
Создание нового вектора
Чтобы создать новый пустой вектор, мы вызываем функцию Vec::new, как
показано в листинге 8-1.
fn main() {
let v: Vec<i32> = Vec::new();
}
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];
}
Поскольку мы задали начальные значения 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);
}
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."),
}
}
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);
}
Когда мы запустим этот код, первый способ с [] вызовет панику программы,
потому что он ссылается на несуществующий элемент. Этот способ лучше
использовать, когда вы хотите, чтобы программа аварийно завершилась при
попытке обратиться к элементу за концом вектора.
Когда методу 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}");
}
Компиляция этого кода приведет к такой ошибке:
$ 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>смотрите в «Rustonomicon».
Итерация по значениям в векторе
Чтобы получить доступ к каждому элементу вектора по очереди, мы должны
итерироваться по всем элементам, а не использовать индексы для доступа к одному
элементу за раз. Листинг 8-7 показывает, как использовать цикл for, чтобы
получить неизменяемые ссылки на каждый элемент в векторе значений i32 и
вывести их.
fn main() {
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
}
forМы также можем итерироваться по изменяемым ссылкам на каждый элемент в
изменяемом векторе, чтобы изменить все элементы. Цикл 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, предотвращает одновременное изменение всего
вектора.
Использование enum для хранения нескольких типов
Векторы могут хранить только значения одного и того же типа. Это может быть неудобно; определенно существуют случаи, когда нужно хранить список элементов разных типов. К счастью, варианты enum определены внутри одного и того же типа enum, поэтому, когда нам нужен один тип для представления элементов разных типов, можно определить и использовать enum!
Например, допустим, мы хотим получить значения из строки электронной таблицы, где некоторые столбцы строки содержат целые числа, некоторые – числа с плавающей точкой, а некоторые – строки. Мы можем определить enum, варианты которого будут хранить разные типы значений, и все варианты enum будут считаться одним и тем же типом: типом этого enum. Затем мы можем создать вектор для хранения этого enum и тем самым в конечном итоге хранить разные типы. Мы продемонстрировали это в листинге 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),
];
}
Rust должен знать во время компиляции, какие типы будут находиться в векторе,
чтобы точно понимать, сколько памяти в куче потребуется для хранения каждого
элемента. Мы также должны явно указать, какие типы разрешены в этом векторе.
Если бы Rust позволял вектору хранить любой тип, существовала бы вероятность,
что один или несколько типов вызовут ошибки при операциях, выполняемых над
элементами вектора. Использование enum вместе с выражением match означает,
что Rust во время компиляции гарантирует обработку каждого возможного случая,
как обсуждалось в главе 6.
Если вы не знаете исчерпывающий набор типов, которые программа получит во время выполнения для хранения в векторе, прием с enum не сработает. Вместо этого можно использовать трейт-объект, который мы рассмотрим в главе 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
}
Когда вектор освобождается, все его содержимое тоже освобождается, то есть целые числа, которые он хранит, будут очищены. Проверщик заимствований гарантирует, что любые ссылки на содержимое вектора используются только пока сам вектор действителен.
Перейдем к следующему типу коллекции: String!