Объясните механизм двухфазного изменяемого заимствования в вызове метода, где аргумент вычисляется из того же объекта.
Двухфазное изменяемое заимствование позволяет сначала зарезервировать изменяемый доступ к объекту, затем вычислить аргументы с использованием совместимых неизменяемых заимствований и только после этого активировать изменяемое заимствование. Поэтому вызов метода может одновременно подготовить изменяемый self и прочитать данные из того же объекта.
Это не отменяет правил заимствования: после активации изменяемого заимствования другие заимствования, несовместимые с ним, недопустимы.
Модель владения Rust должна обеспечивать безопасность памяти без сборщика мусора и без разрешения потенциально конфликтующих доступов к одним данным. Строгое правило для &mut хорошо защищает от гонок и некорректного наложения чтения на запись, но при буквальном применении иногда отвергало бы безопасные выражения в вызовах методов.
Двухфазное заимствование было введено как компромисс между безопасностью и эргономикой. Оно не ослабляет проверку конфликтов во время фактического изменения данных, а разделяет подготовку заимствования и момент его использования.
При вызове метода изменяемая ссылка на получателя обычно создаётся компилятором неявно. Одновременно аргумент метода может вычисляться через неизменяемое заимствование того же объекта.
Если считать изменяемую ссылку активной сразу при начале выражения, эти действия выглядели бы конфликтующими: объект уже нужен для записи, но аргумент ещё требует чтения. Отказ от такого вызова был бы безопасным, но излишне ограничительным; разрешение обычного одновременного чтения и записи, напротив, нарушило бы правила безопасной памяти.
В двухфазной модели неявное изменяемое заимствование проходит две стадии:
Между этими моментами разрешены совместимые неизменяемые заимствования. При этом другое изменяемое заимствование не допускается, поскольку два потенциальных владельца записи уже могли бы конфликтовать.
В выражении values.push(values.len()) компилятор сначала резервирует изменяемый доступ к values для получателя метода push. Затем вычисляется аргумент values.len(), которому нужен только неизменяемый доступ. После завершения вычисления аргумента резервирование активируется, и push изменяет вектор.
Механизм применяется не к любой явно созданной &mut и не превращает изменяемые и неизменяемые ссылки в совместимые вообще. Например, явное длительное изменяемое заимствование обычно считается активным сразу, поэтому нельзя рассчитывать на двухфазное поведение как на универсальный способ обойти конфликт заимствований.
Важна также граница между вычислением аргументов и выполнением тела метода: чтение происходит до начала самой мутации. Если метод уже начал изменять объект, параллельное чтение из него всё равно было бы запрещено правилами Rust.
В коде обработки буфера нужно добавить в Vec его текущий размер. Возможны три решения.
Первый вариант — заранее вычислить размер в отдельной переменной. Он наиболее очевиден и хорошо подходит для сложных выражений, но создаёт дополнительное имя и иногда разрывает компактную логику операции.
Второй вариант — создать явную изменяемую ссылку на вектор, а затем отдельно читать размер. Такой код подчёркивает владение ссылкой, но явное &mut может стать активным раньше, чем нужно, и привести к ошибке заимствования.
Третий вариант — использовать вызов метода с неявным изменяемым получателем и прочитать размер в аргументе. Благодаря двухфазному заимствованию этот вариант безопасен и компилируется, поэтому он выбран для короткой операции, где порядок вычислений очевиден.
Результат — вектор получает новый элемент, а чтение его длины завершается до активации изменяемого доступа. Для более сложной логики предпочтительнее первый вариант: он легче читается и позволяет явно контролировать время вычисления данных.
Вопрос: Разрешает ли двухфазное заимствование два изменяемых доступа к одному объекту до входа в тело метода?
Ответ: Нет. На стадии резервирования допускаются совместимые неизменяемые заимствования, но второе изменяемое заимствование всё равно конфликтует с первым. Иначе компилятор не мог бы гарантировать отсутствие двух потенциальных записей в один объект.
Вопрос: Почему явное создание &mut не всегда ведёт себя так же, как изменяемый получатель метода?
Ответ: Двухфазное заимствование предназначено прежде всего для определённых неявных изменяемых заимствований, которые компилятор создаёт при вызове метода. Явная ссылка обычно считается активной в месте её создания, поскольку программист явно запросил доступ и может использовать эту ссылку в последующих выражениях. Поэтому перенос логики из вызова метода в отдельную переменную со ссылкой &mut способен изменить результат проверки заимствований.
Вопрос: Что произойдёт, если неизменяемое заимствование аргумента сохранится до момента выполнения тела метода?
Ответ: Оно не сможет сосуществовать с активным изменяемым заимствованием. Компилятор разрешает чтение только в промежутке между резервированием и активацией; перед активацией все конфликтующие неизменяемые заимствования должны завершиться. Если область использования такого заимствования действительно продолжается во время вызова, выражение будет отклонено, поскольку двухфазность не разрешает одновременное активное чтение и изменение.