$ sudo teach IT
Модуль 5 · Коллекции · Урок 5.1

Векторы

Вы научитесь хранить сколько угодно трат в одном списке: добавлять, обходить, доставать и печатать их с номерами.

Теория + практика~30 минутСредний

К этому моменту «Копилка» умеет разобрать команду и собрать из неё настоящую трату — значение типа Expense с суммой, категорией и заметкой. Но есть проблема: после печати трата тут же исчезает, а команде список показывать нечего. В массиве [u32; 3] из первого модуля ровно три места, и размер не поменять. Сегодня познакомимся с вектором — списком, который растёт вместе с вашими тратами, и наконец заставим «Копилку» помнить всё, что вы ей сказали.

Вектор: массив, который растёт

Представьте коробку для яиц на десять ячеек: больше десяти яиц в неё не положить, а если яиц три, семь ячеек так и стоят пустыми. Это массив: размер известен заранее и навсегда. Теперь представьте папку-скоросшиватель: добавили листок — папка стала толще, вынули листок — тоньше. Сколько листов, столько и места. Это вектор.

Официальное название — Vec (от слова vector). Вектор хранит любое количество значений одного типа, одно за другим, и умеет расти и уменьшаться, пока программа работает. Сами значения лежат в куче — области памяти, размер которой можно менять на ходу, в отличие от стека с его жёстко отмеренными ячейками. Запись в угловых скобках говорит, что именно лежит внутри: Vec<u32> читаем как «вектор чисел u32», а Vec<Expense> — «вектор трат». Свои обобщённые типы мы писать не будем, нам достаточно уметь читать такую запись.

Создать вектор можно двумя способами: пустой через Vec::new() или сразу с содержимым через макрос vec![...] (восклицательный знак, как у println!, значит макрос).

Rust · main.rs
let mut amounts: Vec<u32> = Vec::new();
let planned: Vec<u32> = vec![350, 1200, 99];
println!("{:?}", planned);
println!("Трат: {}", planned.len());
Вывод
[350, 1200, 99]
Трат: 3

Разбор по строкам

  • let mut amounts: Vec<u32> = Vec::new(); — пустой вектор чисел. Слово mut обязательно: мы собираемся его менять. Тип написан явно, потому что Rust пока не из чего его угадать.
  • vec![350, 1200, 99] — вектор из трёх готовых чисел; квадратные скобки здесь те же, что у массива, но результат — растущий Vec, а не массив.
  • {:?} — отладочная печать (вы уже знаете её по массивам): вектор выводится в квадратных скобках через запятую.
  • planned.len() — сколько сейчас элементов в векторе; считается от единицы, поэтому для трёх чисел будет 3.

Почему нужна подсказка типа

Если написать let amounts = Vec::new(); и дальше ничего в вектор не класть, компилятор ругается: error[E0282]: type annotations needed for `Vec<_>` — он не знает, трат или чисел будет вектор. Стоит добавить : Vec<u32> или первый push со значением, и ошибка пропадает.

Добавить и убрать

Главное умение вектора — меняться. Новое значение кладётся в конец методом push (по-английски «втолкнуть»). Последнее значение можно забрать методом pop («выскочить»): вектор станет короче, а вы получите то, что убрали. Но вдруг вектор пуст и забирать нечего? Тогда pop не падает, а честно отвечает «ничего нет»: он возвращает Option — коробку, в которой лежит либо Some(значение), либо None. Подробно про Option вы уже говорили в модуле про свои типы, здесь он встречается снова.

Убрать элемент из середины — remove(номер): он вынимает элемент с этим номером, возвращает его, а все, что стояло правее, сдвигаются на одно место влево, чтобы не осталось дырки. Узнать размер — len(), спросить, пуст ли вектор, — is_empty().

Rust · main.rs
let mut amounts: Vec<u32> = Vec::new();
amounts.push(350);
amounts.push(1200);
amounts.push(99);
println!("Трат: {}", amounts.len());
let last = amounts.pop();
println!("{:?}", last);
let first = amounts.remove(0);
println!("{}", first);
println!("{:?} {}", amounts, amounts.is_empty());
Вывод
Трат: 3
Some(99)
350
[1200] false

Разбор по строкам

  • amounts.push(350); — кладём 350 в конец; после трёх таких вызовов вектор равен [350, 1200, 99].
  • amounts.pop() — убирает последнее число (99) и возвращает Some(99). Теперь в векторе два числа.
  • amounts.remove(0) — убирает число под номером 0, то есть первое (350), и отдаёт его; оставшееся 1200 сдвигается на место 0.
  • amounts.is_empty() — false, потому что в векторе ещё лежит 1200.

Шпаргалка по методам

МетодЧто делаетЧто возвращает
push(x)кладёт значение в конецничего
pop()убирает последний элементOption: Some(x) или None у пустого
remove(i)убирает элемент с номером i, остальные сдвигаютсяубранный элемент; паника, если номера нет
len()считает элементычисло
is_empty()проверяет, что элементов нетtrue или false

Номера начинаются с нуля

Первый элемент вектора имеет номер 0, последний — len() - 1. Поэтому remove(0) убирает первый элемент, а remove(3) у вектора из трёх элементов — это уже паника: такого номера нет.

Достать элемент: паника или Option

Допустим, вам нужна вторая трата из списка. Есть два способа спросить. Первый — квадратные скобки с номером, как у массива: amounts[1]. Если номер правильный, получите значение. А если вектор короче и такого номера нет? Программа остановится с паникой: «Вы попросили десятый этаж в трёхэтажном доме, лифт сломался».

Второй способ — метод get(номер). Он ведёт себя как вежливый вопрос «а есть ли у вас такой этаж?» и возвращает Option: Some(значение), если номер есть, и None, если нет. Паники не будет, а как поступить в случае None, решаете вы через match.

Rust · main.rs
let planned: Vec<u32> = vec![350, 1200, 99];
println!("{}", planned[1]);
match planned.get(5) {
    Some(a) => println!("Есть: {a}"),
    None => println!("Такой траты нет"),
}
Вывод
1200
Такой траты нет

Разбор по строкам

  • planned[1] — значение под номером 1, то есть вторая трата: 1200. Номер 1, потому что считаем с нуля.
  • planned.get(5) — спрашиваем номер 5; в векторе всего три места, поэтому приходит None.
  • Some(a) => ... — ветка для случая, когда значение нашлось; a — это найденное число.
  • None => ... — ветка для «такого номера нет». Обе ветки обязательны, как и в любом match по Option.
Вывод
thread 'main' panicked at src/main.rs:4:20:
index out of bounds: the len is 3 but the index is 10

Это ровно то, что случится при planned[10]: программа печатает сообщение «индекс вне границ: длина 3, а номер 10» и завершается. Компилятор такую ошибку заранее не увидит, потому что размер вектора известен только во время работы (у массива с константным номером, как мы помним, было иначе).

Что выбрать

Если номер пришёл от пользователя или считается где-то далеко от вектора — берите get и обрабатывайте None. Если вы только что сами проверили границы или номер заведомо верный (первый элемент непустого вектора) — [] короче и честнее.

Пройтись по всем элементам

Хранить траты мало, их надо перебирать: напечатать все, сложить, найти нужную. Для этого есть уже знакомый цикл for. Но тут важна одна буква: for a in &amounts с амперсандом или for a in amounts без.

Аналогия с библиотекой. Записаться в библиотеку и взять книгу почитать — вы вернёте её, и она снова на полке. А если книгу насовсем отдали, на полке пусто. Амперсанд & — это «возьму почитать» (заимствование, ссылка), а запись без него — «забираю насовсем» (перемещение владения, move). Цикл по amounts без & съедает вектор целиком, и после цикла его уже не использовать.

Rust · main.rs
let planned: Vec<u32> = vec![350, 1200, 99];
for a in &planned {
    println!("{a}");
}
println!("Всего {}", planned.len());
Вывод
350
1200
99
Всего 3

Разбор по строкам

  • for a in &planned — перебираем траты по очереди, заимствуя вектор. На каждом шаге a — ссылка на очередное число.
  • println!("{a}") — печать работает со ссылкой так же, как с самим числом.
  • planned.len() после цикла работает, потому что вектор остался у нас: мы его только «брали почитать».

А если нужно не читать, а менять каждый элемент? Тогда заимствуем с правом записи: &mut. Сама ссылка не число, поэтому, чтобы добраться до значения, перед ней ставят звёздочку * — вы уже делали так в *spent += amount.

Rust · main.rs
let mut amounts: Vec<u32> = vec![350, 1200, 99];
for a in &mut amounts {
    *a += 10;
}
println!("{:?}", amounts);
Вывод
[360, 1210, 109]

Разбор по строкам

  • let mut amounts — вектор изменяемый, иначе менять его элементы нельзя.
  • for a in &mut amounts — заимствуем с правом записи; a указывает на каждый элемент по очереди.
  • *a += 10 — «по ссылке a идём к числу и прибавляем 10».

Нумерация: enumerate

Для команды список нам нужны номера: «1. 350 руб., ...». Метод iter() делает из вектора поток ссылок на элементы (for a in &v — короткая запись того же), а enumerate() нумерует этот поток: на каждом шаге вы получаете пару «номер, элемент». Эту пару в заголовке цикла можно сразу разобрать на две переменные — так же, как вы разбирали кортеж в первом модуле: (i, a).

Rust · main.rs
let amounts: Vec<u32> = vec![360, 1210, 109];
for (i, a) in amounts.iter().enumerate() {
    println!("{}. {} руб.", i + 1, a);
}
Вывод
1. 360 руб.
2. 1210 руб.
3. 109 руб.

Разбор по строкам

  • amounts.iter() — перебор по ссылкам, без перемещения вектора.
  • .enumerate() — добавляет к каждому элементу его номер, начиная с 0.
  • (i, a) — i получает номер, a — ссылку на значение.
  • i + 1 — люди считают с единицы, поэтому к номеру прибавляем 1. Сам номер в векторе при этом не меняется.

Вектор и владение

Вектор — владелец своих элементов. Когда вы кладёте в него значение через push, значение переезжает (move) в вектор, как книга, которую вы насовсем отдали в библиотеку. Своей переменной у вас больше нет. Для чисел это незаметно (они копируются), но трата Expense содержит String, поэтому она именно переезжает.

Rust · main.rs
let lunch = Expense::new(350, Category::Food, "обед");
let mut expenses: Vec<Expense> = Vec::new();
expenses.push(lunch);
println!("{}", lunch.describe());
Ошибка компилятора
error[E0382]: borrow of moved value: `lunch`
  |
6 |     expenses.push(lunch);
  |                   ----- value moved here
7 |     println!("{}", lunch.describe());
  |                    ^^^^^ value borrowed here after move

Ничего страшного: нужное теперь лежит в векторе, и обращаться к нему надо там — expenses[0].describe() или через цикл. А если трата нужна ещё и отдельно, это отдельный разговор про клонирование, которое вы видели ещё в модуле про заимствование.

Как передавать вектор в функцию

Если написать параметр expenses: Vec<Expense>, функция заберёт весь вектор себе, и вызывающий его потеряет, как и в случае с String. Нам нужно только прочитать, поэтому просим заимствование. И пишем не &Vec<Expense>, а срез &[Expense] — «ссылка на последовательность трат». Это тот же приём, что с &str вместо &String: такая функция примет и вектор, и часть вектора, и массив. Передавая вектор, пишем &expenses — Rust сам превратит ссылку на вектор в срез.

А если функция должна менять вектор — например, добавлять или удалять элементы, — понадобится &mut Vec<Expense>: срез длину изменить не умеет, а push и remove — методы именно вектора.

Что хотимТип параметраКак вызываем
только читать (печать, подсчёт)&[Expense]print_list(&expenses)
менять элементы или длину&mut Vec<Expense>add_all(&mut expenses)
отдать вектор насовсемVec<Expense>consume(expenses) — после этого вектора у нас нет

Практика: «Копилка» запоминает траты

Подключаем вектор к «Копилке». Откройте src/main.rs в проекте kopilka — он такой, каким вы оставили его в конце модуля про свои типы (заголовок «Копилка 0.4»). Менять будем четыре места: добавим функцию печати списка, заведём вектор, будем складывать в него новые траты и подключим команду список.

Шаг 1. Функция print_list

Вставьте её над fn main(), рядом с print_help:

Rust · main.rs
fn print_list(expenses: &[Expense]) {
    if expenses.is_empty() {
        println!("Пока нет трат");
    } else {
        for (i, expense) in expenses.iter().enumerate() {
            println!("{}. {}", i + 1, expense.describe());
        }
    }
}

Разбор по строкам

  • expenses: &[Expense] — функция только читает траты, поэтому берёт срез.
  • expenses.is_empty() — пустой список печатаем одной понятной строкой, а не молчим.
  • iter().enumerate() и (i, expense) — перебор с номерами, как в теории.
  • i + 1 — нумерация для человека с единицы; expense.describe() — уже готовый метод траты из прошлого модуля.

Шаг 2. Вектор в main

В начале main, рядом с let mut spent: u32 = 0; (до цикла ввода!), добавьте:

Rust · main.rs
let mut expenses: Vec<Expense> = Vec::new();

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

Шаг 3. Складываем траты и подключаем «список»

В match parse_command(&line) дополните ветку Some(Command::Add(expense)): последней строкой в ней должна встать expenses.push(expense);. А строчку про List вставьте выше ветки Some(_) (иначе команду список поймает заглушка Some(_) и ответит «Скоро научимся»). Целиком эти две ветки выглядят так:

Rust · main.rs
Some(Command::Add(expense)) => {
    spent += expense.amount;
    println!("Записано: {}", expense.describe());
    println!("Потрачено: {spent} руб. из {MONTH_LIMIT} руб.");
    print_status(spent, MONTH_LIMIT);
    expenses.push(expense);
}
Some(Command::List) => print_list(&expenses),

Разбор по строкам

  • expense.amount и expense.describe() используем до push: после push трата переехала в вектор, и expense больше не наша.
  • expenses.push(expense) — отправляем трату на хранение, она последняя в списке. Остальные строки ветки (запись, spent, статус) остались как в прошлом модуле.
  • print_list(&expenses) — одалживаем вектор функции для чтения; после вызова он по-прежнему наш.

Остальные ветки (Help, Quit, Some(_), None) не трогайте. Счётчик spent и итоговая строка в конце тоже остаются, как были: пока вектор лишь копит траты, а считать по нему мы научимся в следующих уроках.

Запуск

В терминале, в папке kopilka:

Терминал
cargo run

Введите по порядку: список, затем добавить 350 еда обед в столовой, добавить 120 транспорт метро, снова список и выход. Две первые строки вывода (заголовок и подсказка) в примере опущены.

Вывод
Пока нет трат
Записано: 350 руб., еда, «обед в столовой»
Потрачено: 350 руб. из 30000 руб.
Статус: в норме
Записано: 120 руб., транспорт, «метро»
Потрачено: 470 руб. из 30000 руб.
Статус: в норме
1. 350 руб., еда, «обед в столовой»
2. 120 руб., транспорт, «метро»
Итого потрачено: 470 руб. из 30000 руб.

Если у вас так же, значит, «Копилка» теперь помнит всё, что вы ей сообщили, пока работает.

Проверьте себя

  • Первая команда список до всяких трат печатает «Пока нет трат».
  • Добавьте три траты подряд: в списке три строки с номерами 1, 2, 3, и порядок совпадает с порядком ввода.
  • Уберите у expenses слово mut — прочитайте ошибку про push и верните mut обратно.
  • Переставьте expenses.push(expense); на строчку выше spent += expense.amount; — убедитесь, что компилятор ругается на использование expense после перемещения, и верните порядок.

Частые ошибки

Цикл без амперсанда съел вектор

Что видно: error[E0382]: borrow of moved value: `expenses` и подсказка «`expenses` moved due to this implicit call to `.into_iter()`».

Почему: for e in expenses забирает вектор целиком себе, после цикла его нет.

Как исправить: пишите for e in &expenses (или expenses.iter()), если вектор нужен после цикла.

Нумерация начинается с нуля

Что видно: в списке первая трата подписана «0.», а у вас хотелось «1.».

Почему: номера в векторе идут от нуля, enumerate отдаёт именно их.

Как исправить: печатайте i + 1 — только для показа; в expenses[i] по-прежнему используйте исходный i.

Индекс за границей вектора

Что видно: программа падает: index out of bounds: the len is 3 but the index is 10.

Почему: v[10] не проверяет, есть ли элемент, и при ошибке паникует.

Как исправить: используйте v.get(10) и разберите Some и None через match, либо заранее сравните номер с v.len().

Забыли mut у вектора

Что видно: error[E0596]: cannot borrow `amounts` as mutable, as it is not declared as mutable на строке с push.

Почему: push, pop и remove меняют вектор, а переменная объявлена неизменяемой.

Как исправить: напишите let mut amounts.

Что важно запомнить

  • Vec<T> — растущий список значений одного типа; Vec<Expense> читаем как «вектор трат». Создаём Vec::new() или vec![...], для изменений нужен mut.
  • push кладёт в конец, pop забирает последнее и возвращает Option, remove(i) вынимает по номеру и сдвигает остальное, len и is_empty спрашивают о размере.
  • Номера идут с нуля. v[i] паникует при неверном номере, v.get(i) возвращает Option и не падает.
  • for x in &v читает, for x in &mut v меняет (через *x), iter().enumerate() даёт пары «номер, элемент»; for x in v без амперсанда забирает вектор.
  • push перемещает значение внутрь вектора: после него исходная переменная недоступна.
  • В параметрах читаем через &[T], меняем длину через &mut Vec<T>, отдаём насовсем через Vec<T>.

Вектор хранит траты, но считать по ним мы пока умеем только циклом с накопителем, а отобрать, например, только еду — и вовсе вручную. В следующем уроке познакомимся с итераторами: сумму и фильтры можно будет написать буквально в одну строку.

На следующий урок