В прошлых модулях мы работали с числами, а текст видели лишь в шаблоне чтения с клавиатуры: String::new() и загадочное &mut line. Чтобы обращаться с текстом уверенно, нужно понять главную идею Rust — владение. Она объясняет и этот &mut, и то, почему Rust обходится без сборщика мусора и при этом не даёт сломать память.
Где Rust хранит данные: стек и куча
Представьте две кладовки. Первая — стопка тарелок: кладёте сверху, снимаете сверху, каждая тарелка одного размера, всё быстро и без вопросов. Вторая — большой склад с ячейками: нужно найти свободное место подходящего размера, получить номерок и потом по нему возвращаться. Зато в ячейку можно положить что угодно, а коробку при необходимости заменить на побольше.
Программа устроена так же. Первая кладовка называется стеком (stack): сюда попадают значения заранее известного размера — u32, bool, char. Второе хранилище называется кучей (heap): там живут данные, размер которых меняется по ходу работы, например текст, который пользователь вводит с клавиатуры.
Раньше нам хватало строк в кавычках, вроде "Копилка 0.1": они вшиты в готовую программу и не меняются. Для текста, который можно дописывать и читать с клавиатуры, существует тип String — «строка-владелец». На стеке лежит небольшой ярлык (где текст и сколько его), а сами буквы — в куче.
fn main() {
let amount = 350;
let category = String::from("еда");
println!("{amount} руб., {category}");
}
Разбор по строкам
let amount = 350;— число известного размера, лежит на стеке.String::from("еда")— берёт текст из кавычек и создаёт на его основе свою строку в куче. Читается как «сделатьStringиз этого текста».let category = ...— в переменной лежит ярлык, по которому Rust находит буквы в куче.println!("{amount} руб., {category}");— подставляет оба значения в текст. Выведет350 руб., еда.
Три правила владения
Теперь вопрос: кто убирает за программой? Когда строка в куче больше не нужна, её место надо вернуть. В одних языках это делает сборщик мусора, в других — сам программист, и забытая уборка превращается в утечку памяти. Rust выбрал третий путь: у каждого значения есть владелец, и когда владелец исчезает, значение убирается автоматически.
Бытовая аналогия — ключ от квартиры. Квартира одна, и ключ от неё один. Можно передать ключ другому человеку, но тогда у вас его уже нет. А когда хозяин съезжает и никому ключ не передал, квартиру освобождают. Из этого получаются три правила владения (ownership):
- У каждого значения есть владелец — переменная, в которой оно лежит.
- В каждый момент времени у значения ровно один владелец.
- Когда владелец выходит из области видимости, значение удаляется.
Область видимости (scope) — участок кода между фигурными скобками, где переменная существует. Она начинается со строки с let и заканчивается закрывающей скобкой }. В этой скобке Rust сам вызывает уборку — она называется drop («сбросить»). Писать её не нужно, но знать, что она происходит, нужно.
fn main() {
{
let temp = String::from("временно");
println!("{temp}");
}
println!("{temp}");
}
Внутренние фигурные скобки образуют свой блок. Переменная temp живёт только в нём: на закрывающей скобке происходит drop, и строка из кучи убирается. Последний println! обращается к переменной, которой уже нет:
error[E0425]: cannot find value `temp` in this scope
--> src/main.rs:6:16
|
6 | println!("{temp}");
| ^^^^
|
help: the binding `temp` is available in a different scope in the same function
--> src/main.rs:3:13
|
3 | let temp = String::from("временно");
| ^^^^
Компилятор честно подсказывает: «переменная есть, но в другой области видимости». Это и есть третье правило в действии.
Перемещение: что случается при let b = a
А что если хозяев должно стать двое? В большинстве языков вы просто пишете b = a, и теперь оба имени указывают на одно и то же. В Rust так нельзя, и причина простая. Допустим, a и b владеют одной строкой в куче. Когда они оба выйдут из области видимости, каждый попытается её убрать, и одну и ту же коробку выбросят дважды — это тяжёлая ошибка памяти. Поэтому Rust выбирает по второму правилу: владелец один.
Когда вы пишете let b = a; для String, ключ передаётся: b становится владельцем, а a «опустошается» — пользоваться ей больше нельзя. Это называется перемещением (move). Проверим:
fn main() {
let a = String::from("еда");
let b = a;
println!("{a}");
println!("{b}");
}
Компилятор остановит сборку и объяснит, что случилось. Вот настоящий текст из cargo check:
error[E0382]: borrow of moved value: `a`
--> src/main.rs:4:16
|
2 | let a = String::from("еда");
| - move occurs because `a` has type `String`, which does not implement the `Copy` trait
3 | let b = a;
| - value moved here
4 | println!("{a}");
| ^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let b = a.clone();
| ++++++++
For more information about this error, try `rustc --explain E0382`.
error: could not compile `kopilka` (bin "kopilka") due to 1 previous error
Разбор по строкам
error[E0382]: borrow of moved value: `a`— «использование значения, которое уже перемещено». НомерE0382вы будете встречать часто, запомните его.move occurs because ... does not implement the Copy trait— почему именно перемещение: уStringнет умения копироваться само (оCopy— ниже).value moved here— стрелка на строкуlet b = a;: ключ передали здесь.value borrowed here after move— стрелка наprintln!("{a}"): здесь пытаются воспользоваться уже отданным.help: consider cloning— подсказка с одним из решений; о нём следующий раздел. Последние две строки сообщают, что проект не собран.
Читайте ошибки сверху вниз
Сообщения Rust устроены как маленький рассказ: что случилось, где значение отдали, где пытались взять снова и что можно сделать. Не пугайтесь длины — ответ почти всегда уже написан в подсказках help и note.
Копия: clone и умение Copy
Иногда вам нужны именно два независимых текста. Тогда владелец делает настоящую копию — клонирует значение. Метод clone() создаёт в куче вторую строку с теми же буквами, и у каждой появляется свой владелец:
fn main() {
let a = String::from("еда");
let mut b = a.clone();
b.push_str(" и ужин");
println!("{a} / {b}");
}
Разбор по строкам
a.clone()— создаёт вторую строку-копию.aостаётся владельцем своей,bполучает новую.let mut b—mutнужен, потому что мы будем менять строку (как сspentраньше).b.push_str(" и ужин")— методpush_str(«протолкнуть строку») дописывает текст в конец. Менятьbможно: строки разные.println!выведетеда / еда и ужин: копия изменилась, оригинал нет.
Но почему с числами всё работает без всяких clone? Посмотрите:
fn main() {
let x = 5;
let y = x;
println!("{x} {y}");
}
Выведется 5 5, ошибки нет. Дело в том, что маленькие значения на стеке дёшево скопировать, и Rust делает это сам при каждом присваивании. Такие типы помечены особым умением (трейтом) Copy: целые и дробные числа, bool, char. А вот у String такого умения нет — копировать данные в куче дорого, и Rust не станет делать это втихомолку.
| Тип | Что делает let b = a; | Что с a после |
|---|---|---|
u32, bool, char | копирует | можно пользоваться |
String | перемещает | пользоваться нельзя |
Не лечите clone все ошибки подряд
clone — рабочий инструмент, но каждая копия стоит времени и памяти. Если вставлять его везде «чтобы компилятор отстал», вы ничего не поймёте про владение. Сначала спросите себя: действительно ли мне нужны два независимых значения? В следующем уроке увидим, что чаще достаточно просто дать посмотреть.
Владение и функции
Передача значения в функцию устроена точно так же, как let b = a. Параметр функции — это новая переменная, а аргумент в неё перемещается. Бытовая картина: вы отдали ключ мастеру, и теперь квартира его забота. Когда мастер закончил (функция дошла до }), для строки срабатывает drop.
Если мастер должен вернуть ключ, он возвращает его результатом функции:
fn labeled(note: String) -> String {
format!("[{note}]")
}
fn main() {
let note = String::from("обед");
let result = labeled(note);
println!("{result}");
}
Разбор по строкам
fn labeled(note: String) -> String— функция забирает строку во владение и обещает вернуть другую.format!("[{note}]")— макросformat!работает какprintln!, только не печатает, а возвращает готовуюString. Результат без точки с запятой — он и есть ответ функции.labeled(note)— владение переходит в параметр. После этой строки переменнойnoteвmainпользоваться нельзя.let result— новый владелец результата. Выведется[обед].
Теперь видно неудобство. Если функция только читает строку, например печатает, нам придётся каждый раз либо отдавать и забирать её обратно, либо делать clone. Именно поэтому в Rust есть ссылки: способ дать значение посмотреть, не передавая ключ. Они — тема следующего урока, а пока удобно знать: & перед именем и означает «посмотреть, не забирая». Так, push_str(©) допишет к строке содержимое другой строки, не отбирая её.
Практика: заметка в Копилке
Добавим в проект первую настоящую String: заметку к трате. Увидим на собственном проекте и ошибку, и исправление. Остальное в main.rs (print_header, константа MONTH_LIMIT, print_status, цикл ввода из прошлого модуля) остаётся как было.
- Откройте папку проекта
kopilkaи файлsrc/main.rs. - Выше функции
main, рядом с остальными функциями, добавьте функциюprint_note. - В начало
main, сразу после вызоваprint_header();и до цикла ввода, вставьте три строки про заметку. - В терминале, в папке
kopilka, выполнитеcargo check. Это быстрая проверка без запуска.
fn print_note(note: String) {
println!("Заметка: {note}");
}
let note = String::from("обед");
print_note(note);
println!("Заметка после вызова: {note}");
Получите ошибку, которую уже умеете читать:
error[E0382]: borrow of moved value: `note`
--> src/main.rs:41:38
|
39 | let note = String::from("обед");
| ---- move occurs because `note` has type `String`, which does not implement the `Copy` trait
40 | print_note(note);
| ---- value moved here
41 | println!("Заметка после вызова: {note}");
| ^^^^ value borrowed here after move
|
note: consider changing this parameter type in function `print_note` to borrow instead if owning the value isn't necessary
--> src/main.rs:32:21
|
32 | fn print_note(note: String) {
| ---------- ^^^^^^ this parameter takes ownership of the value
| |
| in this function
help: consider cloning the value if the performance cost is acceptable
|
40 | print_note(note.clone());
| ++++++++
For more information about this error, try `rustc --explain E0382`.
Номера строк у вас могут отличаться. Читаем: note перемещена в print_note (строка с value moved here), а потом мы пытаемся её напечатать. Компилятор даже указывает на параметр String и советует либо взять значение взаймы (это будет в следующем уроке), либо клонировать. Сегодня выберем второе: замените вызов на копию.
let note = String::from("обед");
print_note(note.clone());
println!("Заметка после вызова: {note}");
Запустите программу: cargo run, а на запрос суммы введите 350, затем стоп. Заметка напечатается дважды, потом пойдёт знакомый цикл:
Копилка 0.2
Вводите траты по одной, для выхода напишите стоп.
Заметка: обед
Заметка после вызова: обед
Введите сумму (или стоп):
Записано: 350 руб. Всего: 350 руб.
Введите сумму (или стоп):
Итого потрачено: 350 руб. из 30000 руб.
Осталось: 29650 руб.
Использовано лимита: 1%
Статус: в норме
Две строки про заметку идут сразу после шапки, дальше работает знакомый цикл из прошлого модуля. Главное, чтобы эти две строки шли друг за другом, а ошибки не было.
Проверьте себя
- Вы можете объяснить, на какой строке заметка перестала принадлежать
main. - Вызов
cargo checkсprint_note(note)даётerror[E0382], а сnote.clone()— нет. - Вы понимаете, почему такая же передача обычного числа в функцию не вызвала бы ошибки: числа копируются сами.
- Вы знаете, что
print_noteзабирает строку и удаляет её на закрывающей скобке функции.
Подсказка про следующий шаг
Эта копия — временное решение. В следующем уроке перепишем print_note так, чтобы она получала не саму строку, а ссылку на неё, и clone исчезнет.
Частые ошибки
1. Использовать значение после перемещения
Что видно. error[E0382]: borrow of moved value (иногда use of moved value), стрелки value moved here и value borrowed here after move.
Почему. Строку передали в другую переменную или в функцию, и владельцем стал кто-то другой. Как исправить. Найдите строку value moved here. Если вам нужны два значения — clone(). Если нужно просто посмотреть — ссылка (в следующем уроке). Если старое значение больше не нужно — уберите его использование.
2. Вставлять clone везде «чтобы компилятор отстал»
Что видно. Код компилируется, но в нём десятки .clone(), и непонятно, зачем они.
Почему. Копия — не лекарство, а решение: каждая создаёт новую строку в куче. Как исправить. Для каждого clone задайте вопрос: «нужны ли мне именно два независимых значения?». Если нет — это место для ссылки, о которой следующий урок.
3. Думать, что для чисел let b = a тоже перемещает
Что видно. Ученик пишет let y = x.clone(); для обычного u32 или переживает за x после let y = x;.
Почему. Числа, bool и char имеют умение Copy: они копируются сами, и оба имени остаются живыми. Как исправить. Помните правило: перемещаются только значения, которые владеют данными в куче (пока что — String).
4. Искать переменную за пределами её скобок
Что видно. error[E0425]: cannot find value `temp` in this scope.
Почему. Переменная живёт до закрывающей скобки своего блока, потом значение удалено. Как исправить. Объявите переменную в том блоке, где она нужна, или верните значение наружу как результат блока или функции.
Что важно запомнить
- Стек — для значений известного размера (числа,
bool,char), куча — для растущих данных, какString. - Три правила: у значения есть владелец; владелец один; владелец вышел из области видимости — значение удалено (
drop). let b = a;дляStringперемещает значение:aпосле этого использовать нельзя, иначеerror[E0382]: borrow of moved value.clone()делает настоящую копию в куче: пользуйтесь им осознанно, не по привычке.- Числа,
boolиcharимеют умениеCopyи копируются сами. - Передача в функцию — тоже перемещение; вернуть значение можно результатом функции.
format!собираетStringпо шаблону,push_strдописывает текст в конец изменяемой строки.
Дальше разберёмся, как дать функции прочитать строку, не отбирая её у хозяина, — поговорим про ссылки и заимствование.