В чем разница между escaping и non-escaping замыканиями в Swift?

«В чем разница между escaping и non-escaping замыканиями в Swift?» — вопрос из категории Swift Core, который задают на 10% собеседований IOS Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Разница определяется временем жизни замыкания относительно функции, в которую оно передано.

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

Non-escaping замыкание (поведение по умолчанию) — гарантированно выполняется в пределах работы функции и не может быть сохранено или вызвано позже. Это позволяет компилятору проводить оптимизации.

Ключевые различия:

Аспект Non-escaping (по умолчанию) @escaping
Время жизни В пределах функции Может пережить функцию
Захват self Неявный, без риска цикла ссылок Требует явного захвата ([weak self])
Оптимизация Компилятор может инлайнить Меньше оптимизаций
Использование Синхронные операции (map, filter) Асинхронные колбэки, таймеры, уведомления

Пример non-escaping замыкания:

// Замыкание выполняется синхронно внутри функции
func calculate(values: [Int], using transform: (Int) -> Int) -> [Int] {
    var result: [Int] = []
    for value in values {
        result.append(transform(value)) // Выполняется ДО возврата
    }
    return result
}

// self захватывается неявно, без риска
class Calculator {
    var multiplier = 2

    func process(numbers: [Int]) -> [Int] {
        // non-escaping, можно использовать self без weak
        return calculate(values: numbers) { number in
            return number * self.multiplier
        }
    }
}

Пример @escaping замыкания:

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

// Обязателен явный захват self
class DataLoader {
    var cachedData: Data?

    func load() {
        fetchData(from: someURL) { [weak self] result in
            // weak self для избежания цикла ссылок
            guard let self = self else { return }

            switch result {
            case .success(let data):
                self.cachedData = data
                self.updateUI()
            case .failure(let error):
                self.handleError(error)
            }
        }
    }
}

Правила и best practices:

  1. Всегда используйте non-escaping по умолчанию, если возможно.
  2. Для @escaping замыканий обязательно рассматривайте захват [weak self] или [unowned self] для избежания retain cycles.
  3. С Swift 5+ non-escaping является поведением по умолчанию, а @escaping требуется указывать явно.
  4. Escaping-замыкания могут изменить состояние объекта после его деинициализации, поэтому нужна осторожность.
  5. Некоторые API (например, DispatchQueue.async) всегда требуют escaping-замыканий.