Предскажите вывод программы и объясните, какое состояние разделяют копии замыкания:
func makeCounter() -> () -> Int {
var value = 0
return {
value += 1
return value
}
}
let first = makeCounter()
print(first())
let second = first
print(second())
print(first())
Программа выведет 1, затем 2, затем 3. Копирование замыкания не создаёт независимую копию захваченной переменной value: обе переменные-замыкания обращаются к одному сохранённому состоянию.
Замыкания в Swift могут сохранять локальное состояние функции после её завершения. Это позволяет возвращать функции с поведением и связанными с ними данными без явного создания отдельного класса-счётчика.
Такой подход сочетает удобство функций высшего порядка с контролируемым временем жизни захваченных значений. Когда замыкание копируется, Swift сохраняет доступ к его контексту, чтобы захваченные данные продолжали существовать, пока они нужны замыканию.
Интуитивная ошибка состоит в том, чтобы считать замыкание обычным значением, копирование которого полностью дублирует всё внутреннее состояние. Для замыканий это может привести к неверным ожиданиям: несколько копий способны изменять один и тот же захваченный объект состояния.
Это особенно важно для счётчиков, генераторов, кэшей и обработчиков событий. Если разработчик ожидает независимость копий, но получает общее состояние, результат может зависеть от порядка вызовов и места хранения замыкания.
При выполнении makeCounter создаётся локальная переменная value. Возвращаемое замыкание захватывает эту переменную, поэтому её значение сохраняется после возврата из makeCounter.
Вызов first() увеличивает value с 0 до 1. Присваивание let second = first копирует значение замыкания, но копии продолжают ссылаться на один захваченный контекст; поэтому вызов second() получает и увеличивает то же значение до 2, а следующий вызов first() — до 3.
Это не означает, что все данные, используемые замыканием, всегда становятся общими при любом копировании. Важен сам факт захвата переменной и наличие общего захваченного контекста. Если вызвать makeCounter() дважды, каждый вызов создаст отдельное состояние:
Практическое следствие: копирование замыкания нельзя использовать как способ получить независимый счётчик. Для этого нужно создать новый экземпляр состояния, например повторно вызвать фабричную функцию или явно инкапсулировать состояние в отдельном значимом типе.
Детали внутреннего хранения контекста не следует связывать с конкретной реализацией вроде обязательного «box» в памяти. Надёжный контракт Swift заключается в наблюдаемом поведении захваченной переменной и времени жизни контекста, а не в способе его физического размещения.
Команда реализует два обработчика повторных попыток сетевого запроса. Разработчик создаёт один замыкающий счётчик и передаёт его в два компонента, ожидая, что каждый будет вести собственную статистику. В результате вызовы одного компонента изменяют число попыток, которое видит другой.
Вариант с копированием одного замыкания прост и не требует дополнительного типа, но ошибочен при требовании независимого состояния. Вариант с общей синхронизированной переменной обеспечивает единый глобальный лимит, однако усложняет тестирование и создаёт конкуренцию между компонентами.
Выбранное решение — фабрика, возвращающая новое замыкание для каждого компонента: let counter = makeCounter(). Каждый вызов фабрики создаёт отдельный захваченный контекст, поэтому компоненты не влияют друг на друга. Если обработчики работают из разных конкурентных контекстов, одной независимости счётчиков недостаточно: доступ к разделяемому состоянию всё равно должен соответствовать выбранной модели конкурентности.
Что произойдёт, если замыкание захватит var, а затем эту переменную изменит внешний код?
Замыкание видит изменения той же захваченной переменной, а не снимок её первоначального значения. Захват локальной переменной сохраняет доступ к её изменяемому состоянию. Поэтому важно отличать захват переменной от подстановки её текущего значения в момент создания замыкания.
Создадут ли два вызова makeCounter() общий счётчик?
Нет. Каждый вызов функции создаёт новую переменную value и новый контекст захвата. Поэтому замыкания, возвращённые разными вызовами, имеют независимое состояние, даже если их поведение и исходный код одинаковы.
Почему захваченное состояние не уничтожается сразу после возврата makeCounter()?
Возвращённое замыкание сохраняет необходимый контекст. Пока существует хотя бы доступное замыкание, этот контекст должен оставаться доступным для его вызовов; после уничтожения последней ссылки на замыкание контекст может быть освобождён. Это позволяет локальной переменной пережить стековый вызов функции без превращения её в глобальную переменную.