К этому моменту «Копилка» умеет разобрать команду и собрать из неё настоящую трату — значение типа Expense с суммой, категорией и заметкой. Но есть проблема: после печати трата тут же исчезает, а команде список показывать нечего. В массиве [u32; 3] из первого модуля ровно три места, и размер не поменять. Сегодня познакомимся с вектором — списком, который растёт вместе с вашими тратами, и наконец заставим «Копилку» помнить всё, что вы ей сказали.
Вектор: массив, который растёт
Представьте коробку для яиц на десять ячеек: больше десяти яиц в неё не положить, а если яиц три, семь ячеек так и стоят пустыми. Это массив: размер известен заранее и навсегда. Теперь представьте папку-скоросшиватель: добавили листок — папка стала толще, вынули листок — тоньше. Сколько листов, столько и места. Это вектор.
Официальное название — Vec (от слова vector). Вектор хранит любое количество значений одного типа, одно за другим, и умеет расти и уменьшаться, пока программа работает. Сами значения лежат в куче — области памяти, размер которой можно менять на ходу, в отличие от стека с его жёстко отмеренными ячейками. Запись в угловых скобках говорит, что именно лежит внутри: Vec<u32> читаем как «вектор чисел u32», а Vec<Expense> — «вектор трат». Свои обобщённые типы мы писать не будем, нам достаточно уметь читать такую запись.
Создать вектор можно двумя способами: пустой через Vec::new() или сразу с содержимым через макрос vec![...] (восклицательный знак, как у println!, значит макрос).
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().
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.
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 без & съедает вектор целиком, и после цикла его уже не использовать.
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.
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).
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, поэтому она именно переезжает.
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:
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; (до цикла ввода!), добавьте:
let mut expenses: Vec<Expense> = Vec::new();
Почему до цикла: вектор должен жить всё время работы программы. Если создать его внутри цикла, на каждой команде он будет рождаться пустым заново.
Шаг 3. Складываем траты и подключаем «список»
В match parse_command(&line) дополните ветку Some(Command::Add(expense)): последней строкой в ней должна встать expenses.push(expense);. А строчку про List вставьте выше ветки Some(_) (иначе команду список поймает заглушка Some(_) и ответит «Скоро научимся»). Целиком эти две ветки выглядят так:
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>.
Вектор хранит траты, но считать по ним мы пока умеем только циклом с накопителем, а отобрать, например, только еду — и вовсе вручную. В следующем уроке познакомимся с итераторами: сумму и фильтры можно будет написать буквально в одну строку.