Ты уже объявлял переменные. Давал им имена, присваивал значения, использовал. Где именно они находились в памяти - на какой поверхности, в каком месте - тебя не касалось. Большинство языков и не спрашивают.
Rust тоже не просит тебя управлять этим. Но он рассчитывает, что ты знаешь что к чему.
Стол и доска
Ты сидишь за столом. Поверхность перед тобой ограничена - место для нескольких вещей. Их размеры известны заранее книга, ручка (Они будут такими все рабочее время). Когда этот участок больше не нужен, ты просто смахиваешь всё сразу. Ты не убираешь вещи по одной.
За спиной к стене прикреплена большая пробковая доска. И на ней куда больше места, чем на столе. То, что к ней прикреплено, состоит из частей: связка ключей, набор медалей. Ты можешь добавить или убрать ключ или медаль, когда это требуется. Для каждого такого предмета у тебя есть небольшой листок на столе (ключи - в нижнем правом углу, медали - в центре). Но то, на что он указывает, может быть любого размера. И когда ты убираешь этот листок со стола, тебе нужно снять с доски и то, на что он указывает. Это уже не одно движение - нужно отдельно открепить это от доски.
Стол - это stack. Доска - heap.
Модель, которая почти работает
Рабочая модель: переменная содержит значение. count равно 5,а name - "Alice".
Эта модель верна - и работает в большинстве случаев.
Она ломается, когда типы начинают вести себя по-разному. Почему присваивание целого числа другой
переменной оставляет оригинал нетронутым, а присваивание String - перемещает его? Почему одно
копируется свободно, а другое нет?
Ответ есть и он не в системе типов. Он на столе.
На столе и на доске
let id: u32 = 1; // четыре байта, на столе
let title = String::from("Buy coffee"); // листок на столе, a сам текст прикреплён к доске
id лежит прямо на столе. Четыре байта, размер известен до запуска программы. Когда область
видимости заканчивается, стол очищается одним махом. Скопировать id - значит
записать те же четыре байта, добавить еще один простой предмет на стол: тривиально.
Rust делает это автоматически.
А вот title - не то, чем кажется. Текст "Buy coffee" находится на доске - прикреплён туда,
потому что String может расти, и окончательный размер неизвестен на этапе компиляции.
На столе лежит небольшой листок с тремя пометками: где именно на доске прикреплён текст,
сколько сейчас байт занято и сколько байт в слоте до того, как потребуется больший.
Скопировать title - значит продублировать булавку, текст и слот: а это уже операция у которой
есть своя цена. Rust не делает этого молча. Присваивание title другой переменной передаёт
листок целиком - перемещается только маленький листок, не то, что прикреплено к доске.
Rust помечает оригинал как больше не держащий его.
Копирование title стоило бы слишком дорого - пришлось бы дублировать всё: и булавку, и сам текст. Поэтому Rust не делает этого скрытно. Когда ты присваиваешь title новой переменной, ты просто отдаёшь листок - перемещается только маленькая бумажка, а не то, что приколото к доске. А оригинал? Листок ушёл - Rust больше не даст им воспользоваться.
Значение - не там, где сама переменная.
Когда title покидает область видимости, Rust убирает булавку с доски и освобождает слот.
Никаких действий не требуется - листок держал title, а теперь его нет.
Теперь всё становится на свои места
Чаще всего ты не будешь думать об этом. String, Vec, Box - значения, живущие на доске, -
ведут себя естественно. Rust сам управляет булавками.
Теперь move обретает смысл. title был не просто значением - это был листок который указывал на булавку.
Передать его означало передать ответственность за эту булавку. Копировать “по-тихому” нельзя.
Дальше можно двигаться к borrow checker. Он будет говорить о ссылках(references), которые не должны жить дольше, чем то, на что они указывают, и о конфликтующем доступе к одному месту. Доска, которую он охраняет, - та же самая, что висит у тебя за спиной. Булавки, за которыми он следит, - те самые, о которых ты только что узнал.
Когда появятся сообщения об ошибках, они будут указывать на места, которые ты способен увидеть.
Стоит знать ещё одно: мест больше двух. Строковые литералы - до того, как они становятся
String, - хранятся в статической памяти, встроенной в скомпилированный бинарник. Там же
находится исполняемый код программы. Ни то ни другое не меняется во время выполнения, и ни за
чем из этого Rust не просит тебя следить. Для понимания ownership и borrow checker стол
и доска - достаточная карта.