Разберите ситуацию: во время длительного доступа к значению через параметр inout тот же объект читается другим способом. Почему Swift может завершить выполнение ошибкой эксклюзивного доступа?
Swift требует эксклюзивного доступа к памяти: в один момент времени область значения не должна одновременно изменяться и читаться или изменяться из другого места. Доступ через inout обычно длится не только во время отдельной операции, а весь период вызова функции или замыкания, поэтому параллельное чтение того же значения может вызвать ошибку overlapping access.
Такое правило появилось как часть модели безопасности Swift, чтобы заранее обнаруживать конфликты доступа к изменяемым данным. Оно предотвращает неопределённое поведение, характерное для языков, где один участок программы может менять память, пока другой использует устаревшее или частично изменённое значение.
В Swift это особенно важно для значимых типов: структуры и коллекции обычно изменяются через эксклюзивный доступ, а не через неявное совместное владение изменяемой памятью.
Параметр inout предоставляет функции временный доступ на чтение и запись к исходной переменной. Пока этот доступ активен, другой доступ к той же памяти, включая чтение свойства или элемента коллекции, может пересекаться с ним.
Риск возникает не только при очевидном одновременном присваивании. Доступ может оставаться активным внутри замыкания, вызванного методом, например при работе с буфером коллекции. Попытка прочитать исходную коллекцию внутри такого замыкания нарушает эксклюзивность и может привести к диагностике компилятора или аварийному завершению во время выполнения.
Swift различает три вида доступа: чтение, модификацию и эксклюзивный доступ к изменяемой памяти. Модифицирующий доступ должен быть эксклюзивным: пока он активен, нельзя одновременно читать или изменять тот же участок памяти извне.
Параметр inout не является обычной копией аргумента. На время вызова функция получает контролируемый доступ к исходному хранилищу, а после завершения доступа изменения становятся видны вызывающему коду. Поэтому область конфликта определяется временем действия доступа, а не только строкой, где фактически присваивается новое значение.
Минимальный пример:
Вызов withUnsafeMutableBufferPointer удерживает изменяющий доступ к values на время выполнения замыкания. Обращение к values.count внутри этого периода создаёт пересекающийся доступ к той же коллекции, поэтому программа может получить ошибку эксклюзивного доступа.
Безопасный вариант — получить необходимые данные до начала длительного доступа или использовать уже предоставленное представление:
Это правило не означает, что любое чтение параметра inout внутри функции запрещено. Чтение через сам параметр является частью разрешённого доступа; конфликт возникает, когда тот же участок памяти достигается независимым путём. Для классов ситуация дополнительно зависит от того, к чему относится доступ: к ссылке как к переменной или к состоянию объекта, на который она указывает.
Компилятор способен обнаружить часть конфликтов статически. Динамические проверки нужны для случаев, которые нельзя надёжно определить до выполнения, особенно при работе с коллекциями, указателями, замыканиями и непрямыми обращениями.
В низкоуровневом коде команда оптимизировала обработку массива, передавая его в inout и получая изменяемый буфер для пакетной обработки. Внутри замыкания разработчик обращался к исходному массиву для получения его размера и логирования.
Первый вариант был удобен, но создавал пересечение доступов и мог аварийно завершаться. Полный отказ от буфера упростил код, однако ухудшил контроль над производительностью. Копирование размера до начала доступа оказалось лучшим решением: оно сохранило пакетную модификацию и устранило конфликт без лишней копии массива.
inout означает физическую копию значения?Нет. inout описывает семантику временного доступа к исходному значению, а не обязательное копирование. Компилятор может использовать копирование с последующей записью обратно, если этого требует безопасность или реализация, но полагаться на конкретную физическую стратегию нельзя.
Swift может рассматривать доступ к составному значению как доступ к его общему хранилищу, если границы отдельных участков нельзя безопасно разделить. Например, изменение элемента коллекции и чтение её свойства могут пересекаться на уровне всей коллекции, даже если логически затрагивают разные данные.
Эксклюзивность контролирует пересечение доступов в рамках модели памяти Swift и не заменяет синхронизацию между потоками. Даже если два обращения не нарушают локальное правило эксклюзивности, для совместного доступа из разных потоков всё равно могут потребоваться actor, блокировка или другой механизм синхронизации.