сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Владение и лайфтаймы

Владение и перемещение в 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, остальные разбираются в тренажёре.

  1. #rs_ownership_rules1 / 5
    Почему let a = 5; let b = a; оставляет a доступной, а с String так не выйдет?
    A)Целые реализуют Copy: присваивание дублирует биты, оригинал цел
    B)Числа неизменяемы, поэтому владение к ним не применяется
    C)У i32 нет деструктора, а перемещение работает только с деструкторами
    D)Целые живут на стеке, а всё со стека компилятор копирует автоматически
    показать ответ и разбор
    +A)Целые реализуют Copy: присваивание дублирует биты, оригинал цел

    // разбор: Copy — маркер для типов, у которых побитовая копия полноценно заменяет оригинал: числа, bool, char, ссылки &T, кортежи из таких же типов. Для них присваивание копирует. String владеет буфером в куче, две копии дали бы двойное освобождение, поэтому Copy у неё нет и присваивание перемещает.

  2. #rs_ownership_rules2 / 5
    Что скажет компилятор?
    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 или разбор структуры на части сразу.

  3. #rs_ownership_rules3 / 5
    Структура реализует Drop. Почему из неё нельзя перенести поле (let n = p.name)?
    A)Drop запрещает трогать поля напрямую, только через методы
    B)После перемещения структура станет Copy, а это противоречит Drop
    C)Компилятор запрещает это только для полей, которые сами реализуют Drop
    D)Иначе drop получил бы структуру с дырой на месте перемещённого поля
    показать ответ и разбор
    +D)Иначе drop получил бы структуру с дырой на месте перемещённого поля

    // разбор: Деструктор получает &mut self и вправе рассчитывать на полностью валидный объект. Если бы поле вынесли наружу, drop увидел бы недостроенное значение, а его самого никто не отменял — отсюда E0509. Обходы: обернуть поле в Option и сделать take, либо использовать ManuallyDrop.

  4. #rs_ownership_rules4 / 5
    Что физически делает перемещение String в другую переменную?
    A)Копирует три слова заголовка; буфер в куче остаётся на месте
    B)Меняет владельца в таблице владения, которую ведёт рантайм
    C)Копирует буфер в новое место и освобождает старый
    D)Ставит счётчик ссылок в 1 и переключает указатель на нового владельца
    показать ответ и разбор
    +A)Копирует три слова заголовка; буфер в куче остаётся на месте

    // разбор: String — это указатель, длина и ёмкость: перемещение копирует эти 24 байта, а данные в куче не трогаются. Дальше работает статика: старую переменную компилятор запрещает читать и не вызывает для неё drop, так что двойного освобождения не будет. Рантайм-структур и счётчиков под этим нет — вся бухгалтерия на этапе компиляции.

  5. #rs_ownership_rules5 / 5
    Сколько владельцев может быть у значения одновременно?
    A)Столько, сколько ссылок на него взято
    B)Ровно один — в этом суть правил владения
    C)Один на поток: в каждом потоке своя копия
    D)Ни одного, пока значение не передано в функцию
    показать ответ и разбор
    +B)Ровно один — в этом суть правил владения

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

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.