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-метод — и проблема часто исчезает сама, а код становится читабельнее.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ