Хранение списка значений с помощью векторов
Первым типом коллекции, который мы разберём, будет Vec<T>
, также известный как вектор. Векторы позволяют хранить более одного значения в единой структуре данных, размещающей элементы в памяти один за другим. Векторы могут хранить данные только одного типа. Их удобно использовать, когда нужно хранить список элементов: например, список строк текста файла, или список цен товаров в корзине покупок.
Создание нового вектора
Чтобы создать новый пустой вектор, мы вызываем функцию Vec::new
, как показано в Листинге 8-1.
fn main() { let v: Vec<i32> = Vec::new(); }
Обратите внимание, что здесь мы добавили аннотацию типа. Поскольку мы не помещаем никаких значений в этот вектор, 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); }
Как и с любой переменной, если мы хотим изменить её значение, нам нужно сделать её изменяемой с помощью ключевого слова 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!("Третьего элемента не содержится."), } }
Обратите внимание на пару деталей. Мы используем значение индекса 2
для получения третьего элемента, так как векторы индексируются с нуля. Указывая &
и []
, мы получаем ссылку на элемент по указанному индексу. Когда мы используем метод get
, мы получаем тип Option<&T>
, который мы можем обработать в match
.
Rust предоставляет эти два способа ссылки на элемент, чтобы вы могли выбирать поведение программы при попытке использовать значение индекса за пределами диапазона существующих элементов. В качестве примера, давайте посмотрим, как поведут себя оба способа, если мы будем пытаться получить доступ к элементу с индексом 100 из вектора из пяти элементов:
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!("Первый элемент: {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!("Первый элемент: {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}"); } }
Мы также можем итерироваться по изменяемым ссылкам на каждый элемент изменяемого вектора, чтобы внести изменения во все элементы. Цикл 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
, предотвращает одновременную модификацию всего вектора.
Использование перечислений для хранения множества разных типов
Векторы могут хранить значения только одинакового типа. Это может быть неудобно; определённо могут быть ситуации, когда надо хранить список элементов разных типов. К счастью, варианты перечисления принадлежат к одну и тому же типу перечисления, поэтому, если нам нужен один тип для представления элементов разных типов, мы для этого можем определить и использовать перечисление!
Например, мы хотим получить значения из строки в электронной таблице, где некоторые столбцы строки содержат целые числа, некоторые — числа с плавающей точкой, а другие — строки текста. Можно определить перечисление, варианты которого будут содержать разные типы значений, и тогда все варианты перечисления будут считаться одним и тем же типом — типом самого перечисления. Затем мы можем создать вектор для хранения списка значений этого перечисления и, по сути, для хранения различных типов. Описанное показано Листинге 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), ]; }
Rust должен знать во время компиляции, какие типы будут в векторе, чтобы точно знать сколько, памяти в куче потребуется для хранения каждого элемента. Мы также должны чётко указать, какие типы разрешены в этом векторе. Если бы Rust позволял вектору содержать любой тип, то был бы шанс, что один или несколько типов вызовут ошибки при выполнении операций над элементами вектора. Использование перечисления вместе с выражением match
означает, что во время компиляции Rust гарантирует, что все возможные случаи будут обработаны, как обсуждалось в Главе 6.
Если вы не можете указать исчерпывающий набор типов, которые программе нужно будет хранить в векторе, то техника использования перечисления не сработает. Вместо этого вы можете использовать трейт-объекты, которые мы рассмотрим в главе 17.
Теперь, когда мы обсудили некоторые из наиболее распространённых способов использования векторов, обязательно ознакомьтесь с документацией API, чтобы узнать о множестве полезных методов, определённых для Vec<T>
стандартной библиотекой. Например, в дополнение к методу push
, существует метод pop
, который удаляет и возвращает последний элемент.
Высвобождение вектора высвобождает его элементы
Подобно структурам, вектор высвобождает свою память, когда выходит из области видимости, как показано в Листинге 8-10.
fn main() { { let v = vec![1, 2, 3, 4]; // какая-нибудь работа с v } // <- здесь v покидает область видимости и высвобождается }
Когда вектор высвобождается, всё его содержимое также высвобождается, то есть все числа, хранившиеся в векторе из примера выше, удаляются. Анализатор заимствований гарантирует, что любые ссылки на содержимое вектора используются только тогда, когда сам вектор действителен.
Перейдём к следующей коллекции: String
.