JavaRush /Курсы /Swift SELF /inout и exclusivity of access в Swift

inout и exclusivity of access в Swift

Swift SELF
35 уровень , 2 лекция
Открыта

1. Зачем нужен inout и почему вокруг него правила

Если смотреть на inout без философии, он просто позволяет функции изменить аргумент снаружи, а не только свою локальную копию. Но Swift — язык, который хочет, чтобы такие изменения были максимально предсказуемыми: без «магии», без скрытых побочных эффектов и без ситуаций «иногда работает, иногда ломается после оптимизации».

Когда вы видите inout, вы должны автоматически читать это так: «функция берёт значение во временную аренду и имеет право его менять». И раз это аренда, то у аренды есть правила: нельзя одновременно сдавать в аренду одну и ту же вещь двум людям, если оба собираются её перекрашивать в разные цвета.

В Swift это называется Law of Exclusivity: во время модификации значения должен быть эксклюзивный доступ к соответствующему месту хранения. То есть нельзя, чтобы то же место параллельно читалось или писалось «через другое имя». Именно это правило Swift старается проверять и запрещать в проблемных местах.

2. inout как контракт: & — не «передача по ссылке как в C»

Когда вы объявляете функцию, вы явно фиксируете контракт:

func increment(_ x: inout Int) {
    x += 1
}

А вызываете так:

var n = 10
increment(&n)
print(n) // 11

Здесь важны две идеи.

Первая: символ & в Swift — это не просто «адрес переменной», как в C. Он скорее говорит компилятору: «я понимаю, что это будет мутация исходной переменной, делай это безопасно». В некоторых случаях компилятор вообще может работать с временным буфером, а не с «настоящим адресом» в привычном смысле. Поэтому & — это маркер временного inout-доступа, а не гарантированный «указатель на память».

Вторая: inout — это не «давайте жить без правил, как в C». Наоборот: inout — это тот редкий момент, когда Swift говорит: «Окей, ты хочешь менять чужое значение. Тогда я включаю режим “очень строго”».

3. Exclusivity of access: простая модель поведения

Давайте введём очень бытовую модель, без лезвия в ассемблер. Представьте переменную как шкафчик в раздевалке.

  • Чтение — вы открыли шкафчик, посмотрели, закрыли.
  • Запись — вы открыли шкафчик, переложили вещи, закрыли.

Правило эксклюзивности звучит так: два доступа к одному и тому же шкафчику не должны пересекаться во времени, если хотя бы один из них — запись. То есть «читать + читать» можно, но «читать + писать» нельзя, «писать + писать» — тоже нельзя.

Чтобы это ощущалось не как абстракция, важно понять ещё один термин из описания правила: не мгновенный доступ (non‑instantaneous access). Swift считает, что некоторые операции «держат шкафчик открытым» некоторое время. Например, передача аргумента как inout — это write-доступ, который длится всю продолжительность вызова функции.

Сводная таблица (очень упрощённая, но полезная для головы):

Ситуация Что происходит по смыслу Разрешено?
два чтения одной переменной в одном выражении read + read да
во время inout-вызова читаем/пишем ту же переменную «обходным путём» write + read/write пересекаются нет
один и тот же var передали в два inout параметра write + write пересекаются нет

4. Overlapping access: где Swift запрещает пересечения

Если вы когда-нибудь видели ошибку в духе «overlapping accesses to …, but modification requires exclusive access», то вы уже встречали эту тему. И самое обидное: код может выглядеть «логично», особенно если вы ещё не привыкли, что некоторые доступы для Swift «длинные».

Начнём с классики — нельзя передать один и тот же var дважды как inout.

import Foundation

func swapTwo(_ a: inout Int, _ b: inout Int) {
    let tmp = a
    a = b
    b = tmp
}

var x = 1
swapTwo(&x, &x) // ❌ ошибка: overlapping access

Причина простая: функция говорит «я модифицирую a и b», а вы говорите «a и b — это одно и то же место». Swift запрещает, потому что иначе порядок изменений становится двусмысленным и может меняться после оптимизаций.

Теперь ещё один частый сценарий: элементы массива.

import Foundation

var numbers = [10, 20, 30]
swap(&numbers[0], &numbers[1]) // ❌ часто приводит к конфликту эксклюзивности

Почему? Потому что numbers[0] — это доступ к части массива, но Swift обязан считать, что мутация элемента требует эксклюзивного доступа ко всему массиву (там Copy‑on‑Write и не только). Поэтому два inout-доступа к разным элементам одного массива считаются пересекающимися доступами к одной переменной numbers.

Именно поэтому в Swift есть специальный метод:

import Foundation

var numbers = [10, 20, 30]
numbers.swapAt(0, 1)
print(numbers) // [20, 10, 30]

swapAt устроен так, чтобы не требовать два inout к одному массиву в одном вызове: вы передаёте индексы, а не «две дырки в памяти».

Почему inout — «длинная запись»

Важно не просто запомнить «так нельзя», а понять механику на уровне ощущений.

Когда Swift видит increment(&n), он трактует это как: «на время вызова increment переменная n находится в режиме исключительной модификации». Это означает: если внутри вызова каким-то способом попытаться обратиться к n снова, уже не как к inout-параметру, а «снаружи» — будет конфликт.

Практически это выглядит так: вы передали переменную как inout, и дальше «правильный путь» работать с ней — только через тот параметр, который функция получила (или через $0, если вы внутри замыкания). А вот пытаться читать исходную переменную напрямую — как минимум подозрительно.

Мини-схема: как Swift видит inout-вызов

Чтобы закрепить, полезно визуально представлять границы «длинного доступа». Вот простая блок‑схема:

flowchart TD
    A["Вызов foo(&x)"] --> B["Старт эксклюзивного write-доступа к x"]
    B --> C["Выполняется тело foo: работа с параметром x"]
    C --> D{"Кто-то ещё пытается прочитать/записать x?"}
    D -->|нет| E["ОК, продолжаем"]
    D -->|да| F["Конфликт: overlapping access (ошибка компиляции или runtime trap)"]
    E --> G["Конец foo"]
    G --> H["Завершение write-доступа к x"]

Здесь важно: внутри foo правильный путь работать с x — через сам inout-параметр. Попытка «потрогать x снаружи» в середине — это и есть пересечение доступов, которое Swift запрещает.

5. Практика: inout в приложении и замыканиях

Пример: мутация Library через inout

Чтобы не оставлять inout в вакууме, давайте накинем простой фрагмент домена для нашего CLI‑приложения «библиотека» (мы развиваем его по курсу).

Представим, что у нас есть минимальная модель:

import Foundation

struct Book {
    var id: Int
    var title: String
}

struct Library {
    var books: [Book]
}

Теперь сделаем функцию, которая переименовывает книгу по id. Тут удобно использовать inout, потому что мы хотим менять library «снаружи».

import Foundation

func renameBook(in library: inout Library, id: Int, to newTitle: String) {
    for i in 0..<library.books.count {
        if library.books[i].id == id {
            library.books[i].title = newTitle
            return
        }
    }
}

var lib = Library(books: [Book(id: 1, title: "Swift Basics")])
renameBook(in: &lib, id: 1, to: "Swift 6.2 Basics")
print(lib.books[0].title) // Swift 6.2 Basics

С точки зрения exclusivity здесь всё хорошо: пока идёт вызов renameBook(in: &lib, ...), никто не пытается «снаружи» одновременно читать/писать lib. Вся работа сосредоточена в одном месте, и это хорошо соответствует идее «временной аренды».

inout + обходной доступ через замыкание

Сейчас будет самый коварный момент, который обычно ловит новичков. Код выглядит «ну я же просто чуть-чуть прочитал», но Swift видит пересечение доступов.

Частая ситуация: переменная передана как inout, а внутри замыкания вы обращаетесь к ней по исходному имени. Это может быть конфликтом, потому что inout-доступ всё ещё активен.

Сделаем похожее в маленьком виде:

import Foundation

func modifyTwice(_ value: inout Int, _ body: (inout Int) -> Void) {
    body(&value)
    body(&value)
}

var count = 1
// modifyTwice(&count) { $0 += count } // ❌ потенциальная ошибка эксклюзивности
print(count) // 1

Почему проблема? Потому что count модифицируется через &count (длинная запись), а внутри body вы ещё раз читаете count по исходному имени. Получается пересечение: «мы ещё модифицируем count, а уже читаем его другим способом».

Правильный стиль — «не трогай исходную переменную, если она уже передана как inout». Если нужно значение, скопируй до входа в inout-область:

import Foundation

var count = 1
let captured = count

modifyTwice(&count) { value in
    value += captured
}

print(count) // 3

Здесь мы читаем count один раз до начала «длинного доступа» и используем копию. С точки зрения Swift это безопасно и предсказуемо.

Свойства struct: почему «разные поля» иногда тоже конфликтуют

С struct есть интересный нюанс. Интуитивно хочется думать: «Ну point.x и point.y — разные поля, значит, их можно менять независимо». И часто Swift действительно умеет это распознать в простых случаях: компилятор делает исключения, когда может доказать безопасность для разных stored properties.

Но общая модель всё равно такая: доступ к части value‑типа часто трактуется как доступ к всему значению. Поэтому если вы делаете сложный манёвр, Swift может не суметь доказать, что «поля независимы».

Практический вывод без углубления: когда вы используете inout и работаете с struct, старайтесь держать операции максимально прямолинейными. Если нужно мутировать два поля, лучше сделать mutating-метод внутри структуры или одну функцию, которая получает inout на весь объект и меняет всё внутри — это обычно проще для компилятора и понятнее для человека.

6. Шпаргалка: как лечить ошибки эксклюзивности

В этой части важно не скатиться в «список заклинаний», а увидеть общий принцип: разводим пересекающиеся доступы во времени или в разные места хранения.

Начнём с самого универсального приёма — временная переменная.

Представим, что вам нужно обновить состояние, но вы случайно читаете его «снаружи» во время inout:

import Foundation

func addOldValue(_ x: inout Int, old: Int) {
    x += old
}

var n = 10
let old = n
addOldValue(&n, old: old)
print(n) // 20

Мы заранее вынесли чтение в old, и во время inout-вызова больше к n не обращаемся.

Теперь про массивы: если хотите поменять элементы местами — почти всегда выбирайте swapAt вместо swap(&a[i], &a[j]), потому что swapAt как раз существует, чтобы не провоцировать конфликт эксклюзивности на уровне «двух inout к одному массиву». Это напрямую связано с тем, что стандартные коллекции не получают «особого режима»: мутация элемента требует эксклюзивности всего массива.

Ещё один приём — «делаем одну точку входа мутации». В нашем LibraryCLI это означает: вместо того чтобы пытаться одновременно мутировать library.books и параллельно где-то ещё читать library, лучше собрать всё в один метод, который получает inout Library и внутри делает всё нужное.

Если хочется самоиронии: Swift в этот момент ведёт себя как строгий библиотекарь. Он не против, что вы взяли книгу (переменную), но он очень против, если вы пытаетесь одновременно взять ту же книгу ещё раз — но уже через окошко выдачи, через друга и через чёрный ход.

7. Типичные ошибки

Ошибка №1: воспринимать & как «взять адрес и жить как в C».
Это ведёт к неверным ожиданиям: в Swift & — маркер временного inout-доступа, и компилятор не обязан давать вам стабильный «адрес переменной». В некоторых сценариях может создаваться временная область хранения, а безопасная область действия такого доступа ограничена вызовом.

Ошибка №2: передавать одну и ту же переменную в два inout-параметра.
Новички часто пишут swap(&x, &x) или пытаются вызвать функцию вида foo(&x, &x) «чтобы не заводить вторую переменную». Swift запрещает это, потому что это две пересекающиеся записи в одно и то же место.

Ошибка №3: пытаться делать swap(&array[i], &array[j]) и удивляться, что Swift ругается.
Кажется, что это два разных элемента. Но для Swift это два inout-доступа, которые требуют эксклюзивности целого массива, и поэтому конфликтуют. Решение обычно не в «хитрой магии», а в использовании swapAt(i, j) или перестройке логики.

Ошибка №4: читать исходную переменную внутри замыкания, пока она передана как inout.
Классическая ловушка: modifyTwice(&count) { $0 += count }. Вы думаете «я просто прочитал», а Swift видит пересечение: count уже в режиме эксклюзивной модификации, и читать его «по другому имени» нельзя. Лечение — вынести чтение в локальную копию до входа в inout-область.

Ошибка №5: пытаться «обойти» exclusivity, усложняя выражение, вместо того чтобы упростить его.
С exclusivity почти всегда работает обратное правило: чем прямолинейнее код, тем легче Swift доказать безопасность. Вынесите промежуточные значения в let, разделите выражение на два шага, сделайте одну точку мутации через inout или mutating-метод — и проблема часто исчезает сама, а код становится читабельнее.

1
Задача
Swift SELF, 35 уровень, 2 лекция
Недоступна
Кнопка плюс
Кнопка плюс
1
Задача
Swift SELF, 35 уровень, 2 лекция
Недоступна
Ограничитель уровня
Ограничитель уровня
1
Задача
Swift SELF, 35 уровень, 2 лекция
Недоступна
Обмен местами
Обмен местами
1
Задача
Swift SELF, 35 уровень, 2 лекция
Недоступна
Двойной бафф
Двойной бафф
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ