Swift @escaping и обработчик завершения

100

Я пытаюсь более точно понять «Закрытие» Swift.

Но @escaping и Completion Handlerих слишком сложно понять

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

Это пример кода официальных документов

var completionHandlers: [()->Void] = []

func someFunctionWithEscapingClosure(completionHandler: @escaping ()->Void){
    completionHandlers.append(completionHandler)
}

func someFunctionWithNoneescapingClosure(closure: ()->Void){
    closure()
}

class SomeClass{
    var x:Int = 10
    func doSomething(){
        someFunctionWithEscapingClosure {
            self.x = 100
            //not excute yet
        }
        someFunctionWithNoneescapingClosure {
            x = 200
        }
    }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)

completionHandlers.first?() 
print(instance.x)

Я слышал, что есть два способа и причины использования @escaping

Первый предназначен для хранения закрытия, второй - для операционных целей Async.

Следующие мои вопросы :

Во-первых, если doSomethingвыполняется, тоsomeFunctionWithEscapingClosure будет выполняться с параметром закрытия, и это закрытие будет сохранено в массиве глобальных переменных.

Я думаю, что закрытие {self.x = 100}

Как selfв {self.x = 100}, сохраненном в глобальной переменной, completionHandlersможно подключиться к instanceэтому объектуSomeClass ?

Во-вторых, я так понимаю someFunctionWithEscapingClosure.

Чтобы сохранить закрытие локальной переменной completionHandlerдля глобальной переменной, 'завершенияHandlers we using@ escaping` ключевое слово!

без возврата @escapingключевого слова someFunctionWithEscapingClosureлокальная переменная completionHandlerбудет удалена из памяти

@escaping держать это закрытие в памяти

Это правильно?

Наконец, я просто задаюсь вопросом о существовании этой грамматики.

Возможно, это очень элементарный вопрос.

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

В чем разница между использованием указанного выше шаблона и функцией обратного вызова с экранированием?

Донгкун Ли
источник

Ответы:

124

Обработчик быстрого завершения с экранированием и без экранирования:

Как объясняет Боб Ли в своем блоге Обработчики завершения в Swift с Бобом :

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

Итак, как запустить блок кода только после завершения загрузки? Кроме того, как вы анимируете определенные объекты только после того, как контроллер представления был перемещен на следующий? Что ж, мы собираемся узнать, как создать его как босса.

Судя по моему обширному словарному списку, обработчики завершения означают

Делайте что-нибудь, когда все уже сделано

Сообщение Боба дает ясность в отношении обработчиков завершения (с точки зрения разработчика он точно определяет, что нам нужно понять).

@escaping закрытия:

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

Есть несколько способов избежать закрытия в содержащей функции:

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

  • Асинхронное выполнение: когда вы выполняете закрытие асинхронно в очереди отправки, очередь будет удерживать закрытие в памяти для вас, может использоваться в будущем. В этом случае вы не знаете, когда будет выполнено закрытие.

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

скриншот ошибки

Для большей ясности по этой теме вы можете проверить этот пост на Medium .

Добавим еще один момент, который должен понимать каждый разработчик ios:

  1. Экранирование закрытия : Экранирующее закрытие - это закрытие, которое вызывается после возврата функции, которой оно было передано. Другими словами, он переживает функцию, которой он был передан.
  2. Неэкранированное закрытие : закрытие, которое вызывается в функции, в которую оно было передано, то есть перед возвратом.
Шобхакар Тивари
источник
@shabhakar, что, если мы сохраним замыкание, но не будем вызывать его позже. Или если метод был вызван дважды, но мы вызвали закрытие только один раз. Поскольку мы знаем, что результат такой же.
user1101733
@ user1101733 Я думаю, вы говорите об избежании закрытия, Closure не будет выполняться, пока вы не позвоните. В приведенном выше примере при вызове метода doSomething 2 раза 2 объекта завершенияHandler добавятся в массив завершенияHandlers. Если вы возьмете первый объект из массива завершенияHandlers и вызовете, он будет выполнен, но счетчик массива завершенияHandlers останется прежним (2).
Deepak
@Deepak, Да по поводу закрытия побега. Предположим, мы не используем массив и используем обычную переменную для хранения ссылки на закрытие, так как нам нужно выполнить самый последний вызов. Будет ли какая-то память занята предыдущими закрытиями, которые никогда не вызовут?
user1101733
1
@ user1101733 Замыкание - это ссылочный тип (например, класс), когда вы назначаете новые замыкания переменной, тогда свойство / переменная будет указывать на новое закрытие, поэтому ARC освободит память для предыдущих закрытий.
Дипак
28

Вот небольшой класс примеров, которые я использую, чтобы напомнить себе, как работает @escaping.

class EscapingExamples: NSObject {

    var closure: (() -> Void)?

    func storageExample(with completion: (() -> Void)) {
        //This will produce a compile-time error because `closure` is outside the scope of this
        //function - it's a class-instance level variable - and so it could be called by any other method at
        //any time, even after this function has completed. We need to tell `completion` that it may remain in memory, i.e. `escape` the scope of this
        //function.
        closure = completion
        //Run some function that may call `closure` at some point, but not necessary for the error to show up.
        //runOperation()
    }

    func asyncExample(with completion: (() -> Void)) {
        //This will produce a compile-time error because the completion closure may be called at any time
        //due to the async nature of the call which precedes/encloses it.  We need to tell `completion` that it should
        //stay in memory, i.e.`escape` the scope of this function.
        DispatchQueue.global().async {
            completion()
        }
    }

    func asyncExample2(with completion: (() -> Void)) {
        //The same as the above method - the compiler sees the `@escaping` nature of the
        //closure required by `runAsyncTask()` and tells us we need to allow our own completion
        //closure to be @escaping too. `runAsyncTask`'s completion block will be retained in memory until
        //it is executed, so our completion closure must explicitly do the same.
        runAsyncTask {
            completion()
        }
    }





    func runAsyncTask(completion: @escaping (() -> Void)) {
        DispatchQueue.global().async {
            completion()
        }
    }

}
JamesK
источник
2
Этот код неверен. Не хватает @escapingквалификаторов.
Роб
Мне это понравилось больше всегоi.e. escape the scope of this function.
Гал