Почему для опционального замыкания в Swift не требуется указывать @escaping?

Ответ

Опциональное замыкание (() -> Void)? по умолчанию считается escaping. Явное указание @escaping не требуется, потому что сама опциональная природа (Optional) подразумевает, что замыкание может быть сохранено и вызвано позже, выходя за рамки текущего контекста функции.

Причина: Тип Optional — это перечисление (enum), которое может хранить значение (в данном случае замыкание) как ассоциированный параметр. Поскольку это значение может быть сохранено (например, в свойстве) и использовано асинхронно, компилятор Swift автоматически трактует его как escaping.

Пример:

func performAsyncAction(completion: (() -> Void)?) {
    // Замыкание передаётся в асинхронный контекст -> оно escaping.
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        completion?() // Вызов может произойти после выхода из функции.
    }
}

Контраст с non-escaping: Неопциональные параметры-замыкания по умолчанию non-escaping. Они гарантированно выполняются до выхода из функции, что позволяет компилятору проводить оптимизации.

Ответ 18+ 🔞

Ну, слушай, тут такая история, блядь, с этими замыканиями в Свифте. Как будто в огороде бузина, а в Киеве дядька, но нет — всё логично, если разобраться.

Вот смотри, есть у тебя функция, которая принимает замыкание. Если оно не опциональное — то по умолчанию оно non-escaping. То есть, грубо говоря, функция обещает: «Чувак, я твоё замыкание тут же, на месте, вызову, и всё. Не буду его никуда откладывать, в свойства пихать или в асинхронщину совать». Компилятор этому рад, он может оптимизировать всё нахуй.

А теперь представь, что ты передаёшь замыкание опциональное — (() -> Void)?. И тут, сука, начинается магия. Сам факт, что оно завернуто в Optional (это же просто enum, который может быть .none или .some(closure)), уже намекает системе: «А вдруг я это значение сохраню куда-то? А вдруг вызову потом?». Компилятор смотрит на это и такой: «Да, блядь, логично. Раз опциональное — значит, может утечь за пределы вызова функции». Поэтому он автоматически считает его escaping. Явно писать @escaping уже не нужно — это как говорить «вода мокрая».

Вот живой пример, чтобы вообще всё стало ясно, как божий день:

func performAsyncAction(completion: (() -> Void)?) {
    // Смотри, сука, мы берём это completion (которое может быть nil, а может и не быть)
    // и пихаем его в асинхронную задачу. Оно явно переживёт текущий вызов функции.
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        completion?() // Вызовется через секунду, когда от этой функции и след простынет.
    }
}

Если бы тут было не опциональное замыкание и без @escaping — компилятор бы тебе нахуй матерился, потому что ты пытаешься вынести его за рамки функции. А так — всё легально, всё тип-топ.

Короче, запомни: опциональное замыкание = автоматически escaping. Это не баг, это фича, ёпта. Чтобы ты случайно не пообещал не хранить его, а потом не начал хранить. Компилятор умнее нас с тобой в этом плане, ему виднее.