Если вы в основном занимаетесь кодированием замыканий, чтобы передать их другим функциям как обратные вызовы, возможно, вы еще не столкнулись с концепцией избегания замыкания.

Когда вы отказываетесь от использования API других людей и переходите к созданию собственных (и вы делаете это все время!), это - вот где вы, скорее всего, столкнетесь с концепция избежания закрытия в определенных сценариях.

Я хочу начать с определения термина. Затем я выброшу пару сценариев использования, которые заставят нас задуматься о закрытии, «убегающем».

Определение

Во-первых, определение, не так ли?

Считается, что замыкание экранирует функцию, когда замыкание передается в качестве аргумента функции, но вызывается после возврата из функции.
- Документация разработчика Apple

Таким образом, очевидно, вы можете попасть в ситуацию, когда разрабатываете функцию, которая принимает замыкание в качестве одного из своих параметров:

func doSomething(completion: () -> Void) { ... }

Кроме того, похоже, что можно оказаться в ситуации, когда закрытие будет выполнено, но каким-то образом оно не будет выполнено до после функции, в которую оно было передано возвращается. Это будет примерно так:

  1. Вызовите doSomething и передайте ему замыкание Типа () -> Void
  2. doSomething выполняет свою работу и возвращает
  3. Переданное вами закрытие (закрытие типа () -> Void) выполняется.

Странно, да? Как такое может случиться? Я расскажу об этом через секунду.

На данный момент суть заключается в следующем: всякий раз, когда вы находитесь в подобной ситуации, когда закрытие, которое вы передаете функции, выполняется после возврата функции, которую вы ему передали, , у вас на руках «бегущее закрытие».

Как потребитель API, вы можете не знать или не заботиться об экранировании закрытия.

Как API дизайнер (который, опять же, может быть вами, если вы тот, кто пишет определение doSomething(completion:)), вы имеете заботиться, потому что компилятор Swift будет недоволен ошибками, если вы этого не сделаете.

Так как же возникают сценарии «избегания закрытия»?

Избегание сценариев закрытия

Вот несколько сценариев, по которым можно избежать закрытия.

Сохранение закрытия как состояния

Документы Apple дают пример добавления замыкания, переданного в функцию, к изменяемому массиву замыканий в вашем классе / структуре:

var completionHandlers: [() -> Void] = []
func doSomething(completion: () -> Void){
    completionHandlers.append(completion)
}

Предположительно тогда, спустя некоторое время после возврата doSomething, все обработчики завершения в массиве будут зациклены и выполнены (или что-то в этом роде) ...

Как видите, это следует за паттерном: 1. Завершение прохода, 2. doSomething возврат, 3. Шаблон выполнения закрытия, который у нас был раньше, не так ли?

Итак, это один из сценариев, который может привести к избегающему закрытию, ЕСЛИ вы спроектировали свою систему таким образом.

Каждый раз, когда вы берете замыкание, сохраняете его как состояние, а затем выполняете его позже, замыкание «ускользает» от функции, в которую оно было передано.

Асинхронные асинхронные обратные вызовы

Нет, я там не повторялся. Ну… я сделал, но это было специально. :]

Предположим, вы работаете над своей doSomething(completion:) функцией.

В нем вы вызываете другую функцию, которая выполняет асинхронное действие и запрашивает закрытие завершения самостоятельно.

Что делать, если вы хотите вызвать только обработчик завершения, который был передан в doSomething после завершения асинхронного действия функции other. То есть что, если вы хотите, чтобы два обработчика завершения выполнялись вместе:

func doSomething(completion: () -> Void) {
    doSomeOtherAsynchronousThing(completion: { 
    () -> Void in 
    // code that executes after the other asynchronous thing is done
    completion() 
    }) 
}

Здесь у вас есть вложенное асинхронное поведение, не так ли? Происходит асинхронная асинхронность.

Каждый раз, когда вы откладываете выполнение закрытия на время, после возврата «родительской» функции, у вас на руках «избегающее замыкание».

Объявление «это закрытие бегства!» в коде

Всякий раз, когда вы реализуете функцию, которая вводит возможность того, что закрытие, переданное ей для выхода, вы будете знать об этом.

Компилятор Swift пожалуется, и ваше приложение не построит:

Что вы делаете, чтобы это исправить?

Все очень просто. В строке объявления вашей функции вам нужно добавить атрибут @escaping прямо перед объявлением типа замыкания:

doSomething(completion: @escaping () -> Void)

Подведение итогов

Моей целью было пролить свет на концепцию «избегания закрытий». Я надеюсь, что с определением и примерами сценариев, которые приводят к побегу от замыканий, для вас все стало немного яснее. Отключите звук в комментариях, если вы все еще боретесь или столкнулись с другими сценариями, требующими использования атрибута @escaping!

Первоначально опубликовано на www.andrewcbancroft.com 26 апреля 2017 г.

Эндрю Бэнкрофт является автором нескольких курсов по Pluralsight, связанных с iOS / Swift. Он регулярно ведет блог на www.andrewcbancroft.com, увлечен изучением и открытиями и любит делиться новыми идеями с другими. Эндрю пишет о разработке программного обеспечения как @andrewcbancroft.