При попытке создать изменяемое заимствование, какую роль играет изменяемость переменной владельца?

При попытке создать изменяемое заимствование, какую роль играет изменяемость переменной-владельца?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Переменная-владелец должна быть объявлена с mut, потому что создание &mut T предоставляет эксклюзивное право изменять конкретное место хранения значения. Неизменяемая привязка запрещает такие изменения через неё, даже если сам тип допускает изменение внутреннего состояния.

Важно различать изменяемость привязки и внутреннюю изменяемость типа. mut разрешает менять значение в данной переменной напрямую и выдавать к нему изменяемое заимствование, а Cell или RefCell позволяют менять внутреннее состояние через обычную неизменяемую ссылку по специальным правилам.

Исторический контекст

Rust делает изменяемость явной, чтобы операции записи было легко обнаруживать при чтении кода и проверять на этапе компиляции. Такой подход является частью модели владения и заимствования, которая должна предотвращать use-after-free, двойное освобождение и небезопасное одновременное чтение и изменение памяти без сборщика мусора.

Явная изменяемость также помогает отделить обычную запись в переменную от внутренней изменяемости, когда тип сам управляет доступом к своему состоянию, например через проверку заимствований во время выполнения.

Постановка проблемы

Если значение доступно только через неизменяемую привязку, разрешение на получение &mut позволило бы изменить его через обходной путь. Это нарушило бы обещание, что неизменяемая привязка не меняется обычными операциями языка.

Неверное понимание этого правила приводит к двум типичным ошибкам: попытке получить &mut из переменной без mut и ожиданию, что объявление mut у типа автоматически сделает изменяемыми все его экземпляры. В Rust изменяемость обычно относится к конкретной привязке, а не является свойством типа.

Подробное решение

При создании изменяемого заимствования компилятор проверяет два условия: место хранения должно быть доступно для записи, а на период действия &mut не должно существовать конфликтующих заимствований. Поэтому владелец объявляется изменяемым, если через него требуется выдать изменяемое заимствование.

fn increase(value: &mut i32) { *value += 1; } fn main() { let mut count = 0; let reference = &mut count; increase(reference); println!("{count}"); }

Здесь mut относится к привязке count. Ссылка reference получает исключительный доступ к той же ячейке памяти, а не копию числа. После завершения последнего использования ссылки владелец снова может использоваться напрямую.

let reference = &mut count не означает, что сама переменная reference обязана быть изменяемой: если ссылку не переназначают и не перемещают особым образом, для изменения объекта достаточно разыменования reference. Однако исходное место хранения count должно быть изменяемым, поскольку именно оно является целью записи.

Это правило не следует путать с внутренней изменяемостью. Например, Cell<T> может менять значение через &Cell<T>, потому что сам тип предоставляет безопасный интерфейс такой записи. Для RefCell<T> проверка допустимости изменяемого или неизменяемого доступа выполняется во время выполнения; при нарушении правил возможна паника, а не неопределённое поведение.

Компромисс состоит в том, что явный mut делает обычную изменяемость строгой и статически проверяемой, но иногда требует изменить объявление владельца или передать &mut в функцию. Типы с внутренней изменяемостью гибче для API, однако могут добавлять стоимость проверок во время выполнения и соответствующие ограничения.

Ситуация из практики

В обработчике конфигурации нужно увеличить счётчик через функцию, принимающую &mut i32. Если счётчик объявлен без mut, передача изменяемого заимствования не скомпилируется: владелец не разрешает запись в своё место хранения.

Рассматривались два варианта. Можно было передавать значение по владению и возвращать обновлённый счётчик, но это ухудшало бы интерфейс для составных структур и не выражало бы намерение изменить существующее место. Можно было использовать Cell<i32>, но это имело бы смысл только при необходимости изменять счётчик через неизменяемые ссылки или разделяемый объект.

Выбран обычный &mut и изменяемая привязка владельца. В результате компилятор статически гарантирует эксклюзивность записи, а интерфейс функции явно показывает, что она временно получает право изменить переданное значение.

Что кандидаты часто упускают

  1. Обязательно ли объявлять изменяемой саму переменную-ссылку?

    Нет, это отдельный вопрос. mut у владельца нужен, чтобы получить изменяемую ссылку на его место хранения. mut у переменной, содержащей ссылку, нужен для переназначения этой ссылки или операций, требующих изменяемой самой привязки ссылки. Изменение объекта через уже полученную &mut T не означает автоматической необходимости объявлять переменную-ссылку как mut.

  2. Можно ли изменить значение через неизменяемую ссылку без нарушения этого правила?

    Да, если тип поддерживает внутреннюю изменяемость, например Cell<T>, RefCell<T> или некоторые синхронизирующие типы. В этом случае изменяется не сама привязка и не произвольное значение через обычный &mut, а состояние внутри специального типа через предоставленный им API. Для RefCell<T> совместимость заимствований проверяется во время выполнения.

  3. Достаточно ли mut у владельца, чтобы создать несколько &mut одновременно?

    Нет. mut лишь делает место хранения пригодным для изменяемого заимствования; он не отменяет правило эксклюзивности. Пока действует одно изменяемое заимствование, второе пересекающееся изменяемое заимствование создать нельзя, если компилятор не докажет, что они относятся к непересекающимся частям данных.