Владение и перемещение в Rust
Это сердце языка и первый серьёзный фильтр на собесе. Спрашивают три правила владения, разницу move и Copy, что физически происходит при передаче String в функцию. Проверяют, понимаешь ли ты, что управление памятью здесь целиком статическое: ни сборщика мусора, ни счётчиков по умолчанию.
Стержень: у значения ровно один владелец, конец владения это освобождение, а передача по значению перемещает владение и делает старое имя недоступным.
// Формулировки: «расскажи правила владения», «что происходит при move?», «почему i32 копируется, а String нет?»
Три правила и что из них следует
Правила короткие: у каждого значения есть владелец; владелец в один момент времени один; когда владелец выходит из области видимости, значение уничтожается. Из них вытекает всё остальное, и отсутствие двойного освобождения, и детерминированные моменты уничтожения, и запрет на висячие ссылки.
Передача по значению перемещает владение: после take(s) переменная s недоступна, обращение к ней даёт E0382 - borrow of moved value. Компилятор не обнуляет ничего в рантайме, он просто запрещает пользоваться старым именем.
// Если оригинал нужен дальше - передавай ссылку (&s) или клонируй явно. Клон здесь честный: копирование буфера видно в коде, а не спрятано в присваивании.
- владелец
- переменная, отвечающая за освобождение значения
- E0382
- использование перемещённого значения
Copy против move
Числа, bool, char, ссылки &T и кортежи из таких же типов реализуют Copy: побитовая копия полноценно заменяет оригинал, поэтому присваивание копирует и обе переменные живут дальше. String и Vec владеют буфером в куче - две копии заголовка привели бы к двойному освобождению, поэтому Copy у них нет и присваивание перемещает.
Отсюда связь с Drop: тип с деструктором не может быть Copy. Если бы мог, деструктор вызвался бы для каждой копии, и ресурс освободился бы дважды.
// Физически перемещение String это копирование 24 байт заголовка: указателя, длины и ёмкости. Данные в куче не двигаются, поэтому move дешёвый и предсказуемый.
let a = 5; let b = a; // Copy
let s = String::new();
let t = s; // move
// println!("{s}") — E0382- Copy
- маркер: побитовая копия заменяет оригинал
- move
- передача владения; исходное имя больше не используется
Частичное перемещение
Поле структуры можно переместить отдельно: let name = p.name. Это частичный move - остальные поля продолжают читаться, а вот структура целиком становится непригодной: use of partially moved value.
Если у типа есть реализация Drop, частичное перемещение запрещено вовсе (E0509). Причина простая: деструктор получает &mut self и вправе рассчитывать на целый объект, а его никто не отменял. Обходы - обернуть поле в Option и сделать take, взять std::mem::take для типов с Default или разобрать структуру на части сразу.
// Проверено на компиляторе: без Drop частичный move проходит, с Drop - падает с E0509.
- частичный move
- перемещение отдельного поля структуры
- mem::take
- забрать значение, оставив Default на его месте
Как отвечать: «Что происходит при перемещении String?»
Копируются три слова заголовка - указатель на буфер, длина и ёмкость, всего 24 байта. Данные в куче остаются на месте, никакого копирования содержимого не происходит. Дальше работает статика: компилятор помечает исходную переменную перемещённой, запрещает к ней обращаться и не вставляет для неё вызов drop, поэтому двойного освобождения не будет. Никаких рантайм-структур и счётчиков под этим нет - вся бухгалтерия владения происходит на этапе компиляции.
Ответ показывает, что ты отличаешь move от clone по стоимости и понимаешь, что проверка статическая. Это ровно то, что интервьюер хочет услышать вместо пересказа правил из книги.
На чём валятся
- −Считают, что move копирует данные из кучи. Копируется только заголовок.
- −Думают, что перемещённая переменная становится пустой. Она просто недоступна, запрет статический.
- −Ставят clone на каждый спор с компилятором вместо ссылки или перемещения.
- −Не могут объяснить, почему тип с Drop не может быть Copy.
- −Не знают про частичный move и удивляются, что структура целиком стала недоступной.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.
- Почему let a = 5; let b = a; оставляет a доступной, а с String так не выйдет?A)Целые реализуют Copy: присваивание дублирует биты, оригинал целB)Числа неизменяемы, поэтому владение к ним не применяетсяC)У i32 нет деструктора, а перемещение работает только с деструкторамиD)Целые живут на стеке, а всё со стека компилятор копирует автоматически
показать ответ и разбор
+A)Целые реализуют Copy: присваивание дублирует биты, оригинал цел// разбор: Copy — маркер для типов, у которых побитовая копия полноценно заменяет оригинал: числа, bool, char, ссылки &T, кортежи из таких же типов. Для них присваивание копирует. String владеет буфером в куче, две копии дали бы двойное освобождение, поэтому Copy у неё нет и присваивание перемещает.
- Что скажет компилятор?
struct P { name: String, age: u8 } let p = P { name: s, age: 30 }; let n = p.name; let q = p;A)Соберётся: q получит структуру, где name уже пустая строкаB)Ошибка на let q = p: значение перемещено частичноC)Соберётся: p.name копируется, структура остаётся целойD)Ошибка на let n = p.name: перемещение поля запрещенопоказать ответ и разбор
+B)Ошибка на let q = p: значение перемещено частично// разбор: Перемещать поле по отдельности можно — это частичный move, и дальше живут только оставшиеся поля (p.age читается). А вот структура целиком уже неполноценна: use of partially moved value. Обходят это через клон поля, std::mem::take или разбор структуры на части сразу.
- Структура реализует Drop. Почему из неё нельзя перенести поле (let n = p.name)?A)Drop запрещает трогать поля напрямую, только через методыB)После перемещения структура станет Copy, а это противоречит DropC)Компилятор запрещает это только для полей, которые сами реализуют DropD)Иначе drop получил бы структуру с дырой на месте перемещённого поля
показать ответ и разбор
+D)Иначе drop получил бы структуру с дырой на месте перемещённого поля// разбор: Деструктор получает &mut self и вправе рассчитывать на полностью валидный объект. Если бы поле вынесли наружу, drop увидел бы недостроенное значение, а его самого никто не отменял — отсюда E0509. Обходы: обернуть поле в Option и сделать take, либо использовать ManuallyDrop.
- Что физически делает перемещение String в другую переменную?A)Копирует три слова заголовка; буфер в куче остаётся на местеB)Меняет владельца в таблице владения, которую ведёт рантаймC)Копирует буфер в новое место и освобождает старыйD)Ставит счётчик ссылок в 1 и переключает указатель на нового владельца
показать ответ и разбор
+A)Копирует три слова заголовка; буфер в куче остаётся на месте// разбор: String — это указатель, длина и ёмкость: перемещение копирует эти 24 байта, а данные в куче не трогаются. Дальше работает статика: старую переменную компилятор запрещает читать и не вызывает для неё drop, так что двойного освобождения не будет. Рантайм-структур и счётчиков под этим нет — вся бухгалтерия на этапе компиляции.
- Сколько владельцев может быть у значения одновременно?A)Столько, сколько ссылок на него взятоB)Ровно один — в этом суть правил владенияC)Один на поток: в каждом потоке своя копияD)Ни одного, пока значение не передано в функцию
показать ответ и разбор
+B)Ровно один — в этом суть правил владения// разбор: Правил три: у значения есть владелец, владелец в один момент времени один, с концом его области видимости значение уничтожается. Единственность владельца и убирает двойное освобождение: некому уничтожить дважды. Разделить владение можно явно — Rc или Arc со счётчиком ссылок, но и там уничтожает тот, кто обнулил счётчик.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.