Как Rust определяет, что одновременные заимствования двух разных полей структуры безопасны?
Rust рассматривает разные поля структуры как непересекающиеся области памяти, если компилятор может доказать, что одно поле не является частью другого. Поэтому одно поле можно заимствовать неизменяемо, а другое — изменяемо одновременно. Конфликт возникает только при пересечении фактических областей памяти, а не просто из-за общего владельца.
Модель владения Rust предназначена для предотвращения висячих ссылок, использования освобождённой памяти и гонок данных без сборщика мусора. Чтобы эта модель оставалась практичной, компилятор должен разрешать безопасные операции, которые не создают реального конфликта.
Если бы любое заимствование части структуры блокировало доступ ко всей структуре, обычная работа с независимыми полями стала бы излишне ограниченной. Анализ отдельных полей позволяет сохранить гарантии безопасности и одновременно не заставляет разработчика копировать данные или использовать динамическую синхронизацию.
Предположим, структура содержит большой неизменяемый буфер и изменяемое состояние обхода этого буфера. Логически чтение буфера и изменение позиции не конфликтуют, потому что они обращаются к разным полям.
Наивный запрет такого доступа сделал бы код менее эффективным: пришлось бы временно копировать буфер, менять интерфейс или применять контейнеры с внутренней изменяемостью. Слишком слабое правило, напротив, могло бы допустить одновременное чтение и изменение одной области памяти, что нарушило бы гарантии Rust.
Заимствование в Rust относится не только к переменной-владельцу, но и к конкретному месту в памяти — например, к полю структуры. При обращении к разным полям компилятор проверяет, что эти места не пересекаются, и применяет обычные правила совместимости заимствований отдельно к каждому из них.
Здесь text заимствован неизменяемо, а position — изменяемо. Оба заимствования происходят через state, но относятся к разным полям, поэтому не создают конфликта.
Это не означает, что Rust разрешает любые параллельные заимствования через один объект. Если области памяти могут совпадать, компилятор обязан отклонить операцию. Например, для двух элементов среза, полученных по индексам во время выполнения, он обычно не может доказать, что индексы различны.
Важное ограничение — статическая доказуемость. Человек может знать, что два индекса различны, но компилятор не обязан принимать это знание без соответствующего безопасного API. Для таких случаев применяются методы, явно устанавливающие разделение диапазонов, например split_at_mut.
Механизм не копирует поля и не добавляет проверок во время выполнения. Его компромисс состоит в том, что он эффективно разрешает очевидно непересекающиеся поля, но может потребовать перестроить код, если непересечение выражено динамически или скрыто за интерфейсом, принимающим &mut на всю структуру.
В парсере структура хранит входной текст и текущую позицию. Метод должен прочитать фрагмент текста и обновить позицию. Прямое заимствование независимых полей обычно является лучшим решением: оно не копирует большой буфер, не требует блокировок и сохраняет проверку безопасности на этапе компиляции.
Вариант с копированием текста проще с точки зрения заимствований, но может заметно увеличить потребление памяти и время работы. Вариант с RefCell позволяет проверять заимствования во время выполнения, однако добавляет служебные расходы и может завершиться паникой при нарушении правил.
Если доступ к полям скрыт методом, принимающим &mut self, компилятор может считать, что метод получает изменяемый доступ ко всей структуре. Тогда разумно передать методу отдельные ссылки на нужные поля или разделить структуру на более мелкие компоненты. Выбранный подход с прямым разделением полей сохраняет нулевые дополнительные расходы и даёт результат, проверяемый компилятором.
Почему два разных индекса среза не всегда считаются разными полями?
Индексы среза вычисляются во время выполнения, поэтому компилятор не может в общем случае доказать, что они различны. Если разрешить два изменяемых заимствования без такого доказательства, оба могли бы указывать на один элемент. Поэтому используется безопасный API вроде split_at_mut, который получает границу разделения и возвращает два непересекающихся среза.
Почему вызов метода иногда мешает заимствовать разные поля отдельно?
Метод с получателем &mut self формально получает изменяемое заимствование всего объекта, а не только одного поля. Если одновременно удерживается ссылка на другое поле, такой интерфейс может создать конфликт даже при логически независимых данных. Обычно проблему решают передачей отдельных ссылок в свободную функцию или изменением интерфейса так, чтобы он принимал только нужные поля.
Что изменится, если одно поле содержит ссылку на другое поле той же структуры?
Простого различия имён полей уже недостаточно: нужно проверить не только взаимное пересечение мест, но и допустимость времени жизни вложенной ссылки. Такая самоссылочная структура обычно не может безопасно перемещаться обычным способом, потому что перемещение изменило бы адрес объекта, на который указывает внутреннее поле. Для подобных задач применяют специальные конструкции с закреплением адреса, но это уже отдельная модель владения с дополнительными ограничениями.