Перейти к основному содержимому
  1. Rust/

Как работает Vec в Rust: устройство, методы, оптимизация

2268 слов·11 минут· loading · loading · ·
Rust Dev
Оглавление
about-rust - Эта статья часть цикла.
Часть 21: Эта статья

Вступление
#

Иногда нам нужен список который не зажат в рамки фиксированного размера. В Rust для этого имеется Vec<T> динамический контейнер, который растёт и сжимается по мере необходимости.

Чтобы сделать знакомство с ним более запоминающимся, мы отправимся в путешествие с увлекательным сюжетом.

Главное действующее лицо - капитан космического корабля «Vectoria». Его задача - подготовить корабль к миссии: собрать экипаж, загрузить оборудование, очистить и обслужить системы и наконец отправиться в полёт.

Вместе с капитаном будет робот-помощник RUST-Y или просто Расти. Он умеет писать программы на Rust, но часто совершает типичные ошибки новичка. Капитану придется направлять его и показывать правильные решения.

А ещё в конце статьи вас ждёт испытание: мини-квиз, который покажет насколько хорошо вы справились с миссией и разобрались с Vec.

Готовы? Тогда начнём с самого простого шага: списка экипажа.

Часть 1. Знакомство с Vec — первые шаги
#

Вас вызывают в штаб. Перед стартом новой космической миссии вам поручают задание - предоставить список экипажа. Пока известно только четыре члена команды, ещё двое прибудут позже.

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

Попытка RUST-Y — ошибка новичка
Шаг 1/2
 1fn main() {
 2    // Робот записывает имена по отдельности
 3    let crew1 = "Капитан Нова";
 4    let crew2 = "Инженер Спаркс";
 5    let crew3 = "Доктор Люмен";
 6    let crew4 = "Биолог Рост";
 7
 8    // Он решает: "создам массив"
 9    let crew = [crew1, crew2, crew3, crew4];
10
11    // Но сразу лезет к пятому элементу...
12    let crew5 = crew[4];
13
14    println!("Пятый член экипажа: {crew5}");
15}

Результат: программа упадёт с паникой.

  • Почему? Потому что массив в Rust имеет фиксированный размер — здесь ровно 4 элемента, а доступ к индексу [4] недопустим.
  • 🔹 Паника — это механизм аварийного завершения, когда нарушено условие безопасности (например, выход за границы памяти).

Запустите код в шаге 2 и вы увидите примерно такой вывод:

Экипаж: ["Капитан Нова", "Инженер Спаркс", "Доктор Люмен", "Биолог Рост"]
Длина: 4
Вместимость: 4

Представьте, что у вас есть грузовой отсек корабля.

  • В нём 4 контейнера — это len.
  • Отсек рассчитан максимум на 4 контейнера — это capacity.
  • Если вы добавите ещё один контейнер, корабль “расширит отсек” (реаллокация): выделит новый, более просторный отсек, перенесёт туда все контейнеры и продолжит работу.
Попробую показать это в коде:
 1fn main() {
 2    let mut crew = Vec::new();
 3
 4    for i in 1..=5 {
 5        crew.push(format!("Член экипажа #{i}"));
 6        println!(
 7            "Добавлен {i}-й: len={}, capacity={}",
 8            crew.len(),
 9            crew.capacity()
10        );
11    }
12}

Запустите код и вы увидите, что capacity растёт скачками (примерно в 2 раза).

  • Это нужно, чтобы push в среднем был очень быстрым (O(1)).

Описание методов и конструкций
#

  • push Добавляет элемент в конец вектора. Если вектора не хватает (len == capacity), он автоматически расширяет память.

  • len Возвращает текущее количество элементов в Vec.

  • capacity Показывает, сколько элементов можно ещё добавить без перераспределения памяти.

  • 1..=5 Это диапазон (range). В Rust ..= означает “включительно”, то есть от 1 до 5 включительно. Если бы мы написали 1..5, то получили бы числа 1, 2, 3, 4.

Несколько способов создать Vec
#

В Rust есть несколько способов создать динамический вектор.

1. Пустой вектор
#

Когда мы пока не знаем, какие элементы будут:

let mut crew: Vec<String> = Vec::new();

Здесь мы явно указали тип (Vec<String>). Без него компилятор не поймёт, что именно мы хотим хранить.

2. Макрос vec![]
#

Самый удобный способ, если сразу известны элементы:

let crew = vec!["Алиса", "Борис", "Саяна", "Дмитрий"];

3. С фиксированным числом одинаковых значений
#

Макрос vec! можно использовать и так:

let crew = vec!["Неизвестный"; 3];

Получим вектор [“Неизвестный”, “Неизвестный”, “Неизвестный”].

4. Через итераторы и коллекции
#

Например, можно создать последовательность чисел и собрать её в вектор:

let numbers: Vec<i32> = (1..=5).collect();
println!("{:?}", numbers); // [1, 2, 3, 4, 5]

5. С заранее известной ёмкостью
#

Если мы знаем, что скоро придёт много элементов, выгодно сразу зарезервировать память:

let mut crew: Vec<String> = Vec::with_capacity(10);

Это создаст пустой вектор, у которого len = 0, но capacity = 10.

На практике чаще всего используют vec![], а остальные способы нужны для оптимизаций или специфических случаев.

Часть 2. Проверка экипажа перед тренировкой (паника и безопасный доступ)
#

Перед первой тренировкой капитан просит RUST-Y вызвать члена экипажа по индексу из списка. Но если робот ошибётся с номером, программа завершится аварийно. Это важный момент: такие сбои недопустимы.

Попытка RUST-Y - ошибка новичка
Шаг 1/2
 1fn main() {
 2    let crew = vec![
 3        "Капитан Нова",
 4        "Инженер Спаркс",
 5        "Доктор Люмен",
 6        "Биолог Рост",
 7        "Пилот Вега",
 8        "Связист Хекс",
 9    ];
10
11    // Попытка вызвать участника с индексом 99
12    let who = crew[99]; // ❌ паника
13    println!("На тренировку выходит: {}", who);
14}

Результат: программа упадёт с паникой.

  • Что произошло? В Rust доступ по индексу (crew[99]) проверяется на корректность. Если индекс вне диапазона, программа завершается паникой..
  • В отличие от C/C++, где можно случайно прочитать “левые байты” или сломать память Rust останавливает программу и не даёт ей работать некорректно.

Паника в Rust — это защита и повод проверить ваш код.

Если индекс может быть неверным, используйте .get().

Немного про Option
#

В статье про функциональный Rust мы уже встречались с Option как с типом, который позволяет явно сообщить: “значение может быть, а может не быть” такой аналог null-safety из других языков.

В примере выше Option используется так:

  • Some(value) — элемент в векторе есть.
  • None — элемента нет (например, индекс за пределами).

Это хорошее решение языка: вместо “магических значений” или “падений программы” мы получаем явную модель отсутствия данных.

Часть 3. Личные коды экипажа (итерация по вектору и изменение элементов)
#

Перед полетом необходимо пройти инструктаж и капитан требует, чтобы каждый член экипажа получил личный код доступа в формате: R-XXX Имя

Например:

  • R-001 Капитан Нова
  • R-002 Инженер Спаркс

Робот RUST-Y пробует сделать это с помощью цикла и сразу натыкается на ошибку компилятора.

Попытка RUST-Y
Шаг 1/2
 1fn main() {
 2    let mut names = vec![
 3        String::from("Капитан Нова"),
 4        String::from("Инженер Спаркс"),
 5        String::from("Доктор Люмен"),
 6        String::from("Биолог Рост"),
 7        String::from("Пилот Вега"),
 8        String::from("Связист Хекс"),
 9    ];
10
11    // ❌ Ошибочный подход: iter() даёт неизменяемые ссылки (&String)
12    for (i, name) in names.iter().enumerate() {
13        *name = format!("R-{:03} {}", i + 1, name);
14        println!("Подготовка кода для: {name} (#{})", i + 1);
15    }
16}
17

Почему не компилируется

  • names.iter() возвращает неизменяемые ссылки &String.
  • Изменять значения по таким ссылкам запрещено.
  • Чтобы изменять элементы вектора, нужны изменяемые ссылки &mut String.

Разбор новых понятий
#

iter() vs iter_mut()
#

  • iter(&self) -> Iter<'_, T> — даёт &T (чтение).
  • iter_mut(&mut self) -> IterMut<'_, T> — даёт &mut T (изменение).

⚠️ Пока выполняется iter_mut(), нельзя иметь другие заимствования — это правило единственной изменяемой ссылки в Rust.

enumerate()
#

  • Оборачивает итератор и добавляет индекс.
  • Индексы начинаются с 0, поэтому для “человекопонятной нумерации” используем i + 1.

Изменение элемента через *name
#

  • В цикле name имеет тип &mut String.
  • Чтобы присвоить новое значение, используем разыменование
*name = format!("R-{:03} {}", i + 1, *name);

format!
#

  • Макрос для сборки строк по шаблону.
  • Возвращает новый String.
  • Экономия на ручных конкатенациях (суммирования строк).

Часть 4. Массовая загрузка корабля и проверка приборов
#

Подготовка к миссии выходит на новый уровень.

Корабль Vectoria должен принять на борт сотни контейнеров и провести проверку бортовых приборов.

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

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

Попытка RUST-Y — загрузка
Шаг 1/2
 1use std::time::Instant;
 2
 3fn main() {
 4    // Симулируем поток данных от датчиков
 5    let incoming: Vec<String> = (1..=20_000)
 6        .map(|i| format!("Прибор #{i:05}"))
 7        .collect();
 8
 9    let start = Instant::now();
10    let mut manifest: Vec<String> = Vec::new();
11
12    let mut last_cap = manifest.capacity();
13    let mut reallocs = 0;
14
15    for item in incoming {
16        manifest.push(item); // добавляем по одному
17        if manifest.capacity() != last_cap {
18            reallocs += 1;
19            last_cap = manifest.capacity();
20        }
21    }
22
23    println!(
24        "Загрузка завершена: len={}, capacity={}, reallocs={}, elapsed={:?}",
25        manifest.len(),
26        manifest.capacity(),
27        reallocs,
28        start.elapsed()
29    );
30}

Что происходит:

  • Vec начинает с минимальной ёмкости.
  • При каждом переполнении выделяется новый блок памяти → копирование старых данных.
  • На больших объёмах видно множество реаллокаций.

Разбор кода для 2 шага
#

  • push добавляет по одному элементу.
  • extend добавляет коллекцию или итератор целиком.
  • append(&mut other) переносит данные из другого вектора без копирования (после этого other опустошается).
  • with_capacity(n) сразу резервирует место под n элементов.
  • reserve(n) / reserve_exact(n) — динамическое выделение при работе порциями.

Аналогия с роботом и складом

Представьте складской отсек:

  • RUST-Y тащит ящики один за одним, каждый раз переставляя стены отсека когда не хватает объема помещения (реаллокации).
  • Капитан заранее строит отсек нужного размера — и загрузка идёт без перебоев.

Часть 5. Буфер телеметрии — только последние данные
#

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

Хранить всё подряд — невозможно: память ограничена.

Задача: держать в буфере только последние N записей, а старые сбрасывать.

Расти пишет свой вариант и, как обычно, попадает в ловушку новичка.

Попытка RUST-Y — неограниченный рост
Шаг 1/4
 1fn main() {
 2    let mut telemetry: Vec<i32> = Vec::new();
 3
 4    // Симулируем поток данных
 5    for i in 0..200 {
 6        telemetry.push(i);
 7    }
 8
 9    println!("Собрано {} значений", telemetry.len());
10}
11

Проблема:

  • буфер растёт бесконечно,
  • через некоторое время память будет исчерпана.

Часть 6. Итоги и старт миссии 🚀
#

Итак все системы корабля Vectoria проверены.

  • Экипаж собран и получил личные коды.
  • Приборы загружены и протестированы.
  • Буфер телеметрии настроен чтобы хранить только актуальные данные.

Робот RUST-Y научился многому: он уже не делает типичных ошибок новичка и готов сопровождать миссию в полёте.

Капитан даёт сигнал: “К старту готовы!”

Проверьте себя: пройдите финальный квиз ниже.

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

Спасибо, что прочитали статью про векторы в Rust до конца, надеюсь что смог объяснить как их правильно готовить! :D

🛰️

about-rust - Эта статья часть цикла.
Часть 21: Эта статья

Связанные статьи

BufReader в Rust: ускоряем ввод-вывод
1861 слово·9 минут· loading · loading
Rust Dev
Поиск данных в файле - Cli хелпер для мастера с помощью Rust
1812 слов·9 минут· loading · loading
Rust Dev
Ввод данных от пользователя в Rust: как использовать stdin
344 слов·2 минут· loading · loading
Rust Dev