Является ли опциональное замыкание (optional closure) в Swift escaping по умолчанию?

Ответ

Да, опциональное замыкание неявно является @escaping.

Причина: Компилятор Swift не может гарантировать, что опциональное замыкание будет вызвано синхронно в пределах тела функции. Поскольку замыкание может быть nil, его вызов часто откладывается или выполняется условно, что подразумевает необходимость его сохранения (escaping).

Пример, требующий escaping:

var storedClosure: (() -> Void)? // Для хранения замыкания оно должно быть escaping

func performAsyncTask(completion: (() -> Void)?) {
    // Сохранение в свойство или асинхронный вызов требуют escaping
    storedClosure = completion

    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        completion?() // Вызов после завершения функции — явный признак escaping
    }
}
// Компилятор автоматически трактует `completion` как @escaping в этом случае.

Как сделать его non-escaping? Фактически, нет способа явно объявить опциональное замыкание как @nonescaping. Если нужно non-escaping поведение, следует:

  1. Избегать хранения замыкания.
  2. Вызывать его синхронно и гарантированно внутри функции.
  3. Рассмотреть возможность использования не-опционального замыкания со значением по умолчанию (например, {}).

Ответ 18+ 🔞

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

Почему так, нахуй? Да потому что компилятор — не телепат, ёпта! Он смотрит на твой опциональный completion: (() -> Void)? и такой: «А хули мне думать? Ты можешь его вызвать сейчас, а можешь — никогда, потому что он nil. Или сохранить куда-то, или запулить в асинхронную очередь. Так что, мудила, считай, что оно уже сбежало!». И всё, приехали.

Вот, смотри, как это выглядит в жизни, когда ты пытаешься его удержать, а оно уже в закате:

var storedClosure: (() -> Void)? // Этому свойству похуй на твои non-escaping амбиции

func performAsyncTask(completion: (() -> Void)?) {
    // Пытаешься сохранить? Ага, щас, держи @escaping!
    storedClosure = completion

    // Или вот, классика жанра — отложенный вызов. Функция уже сдохла, а замыкание ещё живое.
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        completion?() // Пиздец, оно уже за пределами тела функции! Явный побег.
    }
}
// Компилятор тут просто смотрит на это и тихо матерится: «Ну конечно, блядь, @escaping».

А как сделать, чтобы оно не сбегало, как последний подлец? Да нихуя не сделать, если оно опциональное! Swift тебе такого счастья не даст. Если тебе прям позарез нужно @nonescaping, то:

  1. Не храни его нигде, блядь! Вызови сразу и на месте, пока функция не кончилась.
  2. Гарантируй, что вызов будет синхронным и внутри функции. Но с опционалом это как танцевать с бубном вокруг nil — можно промахнуться.
  3. Вообще, забей на опционал! Сделай замыкание неопциональным, но с дефолтным пустым значением, типа completion: () -> Void = {}. И будет тебе счастье, и компилятор успокоится.

Вот так вот, ебать мои старые костыли. Казалось бы, мелочь — опциональность, а сколько подводных ебучков вылезает. Swift — он такой, любит сюрпризы, сука.