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

Ответ

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

@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-замыканий.

Ответ 18+ 🔞

А, слушай, вот эта тема с escaping и non-escaping замыканиями — это как раз тот случай, когда теория на практике бьёт по мозгам, если не понять суть. Сейчас разжуём, но без соплей.

Представь себе, что функция — это такой гостеприимный чувак, который зовёт тебя (замыкание) в гости.

Non-escaping (по умолчанию) — это когда ты заходишь, делаешь свои дела (например, умножаешь числа) и уходишь до того, как хозяин закроет за тобой дверь. То есть ты гарантированно отработаешь, пока функция ещё жива. Компилятор это любит, он может оптимизировать, и с self можно не париться — цикла ссылок не будет, ведь ты никуда не сбежишь.

А @escaping — это когда ты заходишь, а хозяин тебе такой: «Знаешь что, чувак? Я сейчас убегу по делам, а ты тут побудь, можешь на диване посидеть, чайку попить, а как я вернусь — ты мне результат отдашь». Или вообще: «Я тебя запишу в свой ежедневник, чтобы позже позвать». Твоё замыкание может пережить саму функцию. Вот поэтому его и помечают @escaping — оно может «сбежать» за пределы её жизни. А раз оно сбежало и где-то там болтается, то с захватом self надо быть осторожным, а то нахуяришь retain cycles, и память потечёт.

Ключевые отличия, если по-простому:

Штука Non-escaping (обычное) @escaping (сбежавшее)
Когда отработает Пока функция не закончилась. Может и после того, как функция давно сдохла.
self в классе Можно юзать без зазрения совести. Обязательно думай: [weak self] или [unowned self], а то прилетит.
Любовь компилятора Очень любит, оптимизирует. Любит меньше, оптимизаций почти нет.
Где юзают Всякие map, filter, reduce — синхронные штуки. Асинхронщина: сетевые запросы, таймеры, уведомления, сохранение в свойство.

Вот тебе пример non-escaping, всё просто:

// Замыкание выполняется прямо здесь и сейчас, до return
func calculate(values: [Int], using transform: (Int) -> Int) -> [Int] {
    var result: [Int] = []
    for value in values {
        result.append(transform(value)) // Всё! Уже отработало.
    }
    return result
}

class Calculator {
    var multiplier = 2

    func process(numbers: [Int]) -> [Int] {
        // non-escaping, можно на self смотреть без опаски
        return calculate(values: numbers) { number in
            return number * self.multiplier // self? Да похуй!
        }
    }
}

А теперь @escaping, тут уже веселее:

// Замыкание completion отложат на потом, после задержки
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()
    // Функция fetchData тут завершается, а completion ещё даже не нюхал вызова
}

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)
            }
        }
    }
}

Главные правила, чтобы не обосраться:

  1. По умолчанию всегда non-escaping. Если можешь сделать так — делай. Компилятор тебя за это похвалит.
  2. Если юзаешь @escaping — сразу думай про [weak self]. Исключения бывают, но сначала всегда предполагай худшее. Иначе объект будет жить вечно, как душа грешника в аду.
  3. С Swift 5+ @escaping нужно писать явно. Раньше было по-другому, но сейчас язык за тебя думает — если не указал, значит non-escaping.
  4. Escaping-замыкание — как письмо в будущее. Оно может прилететь, когда объект уже в могиле. Поэтому обращайся с осторожностью.
  5. Некоторые системные штуки (типа DispatchQueue.async) всегда требуют @escaping. С этим ничего не поделаешь, так уж они устроены, епта.

Вот и вся магия. Не усложняй. Non-escaping — сделал и забыл. Escaping — сделал, отпустил в свободный полёт, но помни, что оно может тебя потом найти.