Когда замыкание в Swift может быть сбегающим, но не помечено как @escaping?

Ответ

Замыкание является сбегающим по семантике, но не требует аннотации @escaping, когда оно выполняется синхронно внутри вызываемой функции до её завершения. Компилятор Swift требует @escaping только если замыкание может быть вызвано после возврата из функции.

Пример без @escaping:

func performSync(closure: () -> Void) {
    // Замыкание выполняется немедленно, до выхода из функции
    closure()
}

performSync {
    print("Выполнено синхронно")
}

Контраст с @escaping:

class Storage {
    var savedClosure: (() -> Void)?

    func storeForLater(closure: @escaping () -> Void) {
        // Замыкание сохраняется и может быть вызвано позже
        savedClosure = closure
    }
}

Ключевое правило: @escaping требуется, когда замыкание:

  1. Сохраняется в свойство/переменную вне scope функции
  2. Передаётся в асинхронную операцию (DispatchQueue, URLSession)
  3. Возвращается как результат функции

Ответ 18+ 🔞

А, слушай, смотри, тут про замыкания в Swift один интересный момент есть, прямо как в анекдоте про немого Герасима — с виду одно, а на деле пиздец какая разница!

Вот представь: замыкание — это как обещание, которое ты даёшь функции. И компилятор, этот дотошный зануда, постоянно спрашивает: «Слушай, а ты это обещание собираешься выполнить прямо сейчас, до того как я отсюда выйду, или потом, когда я уже в жопу уйду?»

Если ты говоришь: «Да я щас, на месте, внатуре!» — то всё, спокойно, можно без всяких аннотаций. Это как сказать «Муму!» и сразу погладить собаку. Замыкание выполнилось синхронно, функция завершилась — все довольны.

func сделайСейчас(задание: () -> Void) {
    // Щас, блядь, на месте! Прямо до того как отсюда выйти.
    задание()
}

сделайСейчас {
    print("Всё, готово, начальник!")
}

А вот если ты начинаешь хитрить, как тот самый злобный чувак из рассказа: «О, я это обещание сохраню на потом, в карман положу, а вызову его, когда ты уже за горизонтом будешь!» — вот тут-то компилятор тебя хватает за жабры и орет: «Так, стопэ, дружок-пирожок! Ты куда это? Это же @escaping нужно, ёпта! Ты ж его после меня вызывать собрался!»

class Хранилище {
    var отложенноеОбещание: (() -> Void)? // Карман для обещаний

    func сохранитьНаПотом(обещание: @escaping () -> Void) {
        // Ага, сука, кладёшь в карман! Я тебя вижу!
        отложенноеОбещание = обещание
    }
}

И главное правило, которое надо выжечь на жопе, чтобы не забыть: @escaping нужно ТОЛЬКО в трёх случаях, когда замыкание:

  1. В карман положил — сохранил в свойство или переменную, которая живёт дольше функции. Пиздец, теперь оно сбежало!
  2. В асинхронную авантюру отправил — передал в DispatchQueue, URLSession или другую штуку, которая говорит «окей, сделаю, но не сейчас, а когда-нибудь». Типа «я потом, блядь, на озере утоплю».
  3. Как подарочек вернул — функция возвращает это замыкание как результат. То есть ты его не выполнил, а выкинул наружу со словами «держи, разберись с этим сам».

А если ты его вызываешь прямо внутри, до того как сказать return — то всё, расслабься, это не сбегающее. Это как Герасим, который сразу говорит «Муму» и всё — никакой трагедии, собака жива, совесть чиста. Компилятор доволен, и ты не мудак.

Вот и вся философия, блядь. Либо делай сейчас, либо помечай @escaping и будь готов к последствиям, как тот самый максималист.