Ответ
Разница определяется временем жизни замыкания относительно функции, в которую оно передано.
@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:
- Всегда используйте non-escaping по умолчанию, если возможно.
- Для
@escapingзамыканий обязательно рассматривайте захват[weak self]или[unowned self]для избежания retain cycles. - С Swift 5+ non-escaping является поведением по умолчанию, а
@escapingтребуется указывать явно. - Escaping-замыкания могут изменить состояние объекта после его деинициализации, поэтому нужна осторожность.
- Некоторые 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)
}
}
}
}
Главные правила, чтобы не обосраться:
- По умолчанию всегда non-escaping. Если можешь сделать так — делай. Компилятор тебя за это похвалит.
- Если юзаешь
@escaping— сразу думай про[weak self]. Исключения бывают, но сначала всегда предполагай худшее. Иначе объект будет жить вечно, как душа грешника в аду. - С Swift 5+
@escapingнужно писать явно. Раньше было по-другому, но сейчас язык за тебя думает — если не указал, значит non-escaping. - Escaping-замыкание — как письмо в будущее. Оно может прилететь, когда объект уже в могиле. Поэтому обращайся с осторожностью.
- Некоторые системные штуки (типа
DispatchQueue.async) всегда требуют@escaping. С этим ничего не поделаешь, так уж они устроены, епта.
Вот и вся магия. Не усложняй. Non-escaping — сделал и забыл. Escaping — сделал, отпустил в свободный полёт, но помни, что оно может тебя потом найти.