Что такое escaping-замыкание (escaping closure) в Swift?

«Что такое escaping-замыкание (escaping closure) в Swift?» — вопрос из категории Swift Core, который задают на 23% собеседований IOS Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Escaping-замыкание — это замыкание, которое может быть вызвано после завершения выполнения функции, в которую оно было передано как аргумент. Для его обозначения используется атрибут @escaping.

Контраст с non-escaping (по умолчанию):

  • Non-escaping: Компилятор гарантирует, что замыкание будет выполнено до выхода из функции. Это позволяет оптимизировать память (не требуется захватывать и хранить контекст).
  • Escaping: Замыкание сохраняется (например, в свойстве, глобальной переменной или передается в другую асинхронную функцию) для последующего вызова. Это требует явного указания @escaping.

Типичные сценарии использования:

  1. Асинхронные операции: Completion-обработчики для сетевых запросов, таймеров, анимаций.
  2. Хранение: Сохранение замыкания в свойстве класса для вызова позже (например, в обработчиках событий).

Пример с сетевым запросом:

func loadData(from url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
    let task = URLSession.shared.dataTask(with: url) { data, response, error in
        // Это замыкание выполняется асинхронно, ПОСЛЕ того как `loadData` вернула управление.
        DispatchQueue.main.async {
            if let error = error {
                completion(.failure(error))
            } else if let data = data {
                completion(.success(data))
            }
        }
    }
    task.resume() // Функция завершается здесь, но `completion` еще не вызван.
}

Критически важные последствия:

  1. Циклы сильных ссылок (Retain Cycles): Поскольку escaping-замыкание переживает функцию, оно явно захватывает (captures) используемые в нем объекты. Если замыкание, хранящееся в свойстве объекта, захватывает self сильно (self.someProperty), возникает цикл удержания.
  2. Обязательное использование weak/unowned: Для предотвращения циклов необходимо явно управлять захватом.
    func setupHandler() {
    someAsyncOperation { [weak self] result in // Захватываем self слабой ссылкой!
        guard let self = self else { return }
        self.updateUI(with: result)
    }
    }
  3. Изменяемость (mutating): Escaping-замыкание не может захватывать изменяемую ссылку self в mutating методе структуры или перечисления.