Экранирование замыканий в Свифте

Я новичок в Свифте, и я читал руководство, когда наткнулся на спасение замыканий. Я не получил описание руководства вообще. Может, кто-нибудь объяснит мне, что такое закрывающиеся замыкания в Swift, в простых терминах.

9 ответов

Решение

Рассмотрим этот класс:

class A {
    var closure: (() -> Void)?
    func someMethod(closure: () -> Void) {
        self.closure = closure
    }
}

someMethod присваивает переданное замыкание свойству в классе.

Теперь вот еще один класс:

class B {
    var number = 0
    var a: A = A()
    func anotherMethod() {
        a.someMethod { self.number = 10 }
    }
}

Если я позвоню anotherMethodзакрытие { self.number = 10 } будет храниться в случае A, поскольку self захвачен в закрытии, случай A также будет иметь сильную ссылку на это.

Это в основном пример сбежавшего закрытия!

Вы, наверное, задаетесь вопросом: "Что? Так откуда же убежало закрытие?"

Замыкание выходит из области действия метода в область действия класса. И это можно назвать позже, даже в другой ветке! Это может вызвать проблемы, если не обрабатывается должным образом.

Чтобы избежать случайного выхода из затворов и вызвать циклы сохранения и другие проблемы, используйте @noescape атрибут:

class A {
    var closure: (() -> Void)?
    func someMethod(@noescape closure: () -> Void) {
    }
}

Теперь, если вы попытаетесь написать self.closure = closureне компилируется!

Обновить:

В Swift 3 все параметры замыкания не могут выходить по умолчанию. Вы должны добавить @escaping атрибут, чтобы замыкание могло выйти из текущей области видимости. Это повышает безопасность вашего кода!

class A {
    var closure: (() -> Void)?
    func someMethod(closure: @escaping () -> Void) {
    }
}

Я иду более простым способом.

Рассмотрим этот пример:

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

Выше указано закрытие без экранирования, потому что замыкание вызывается до возврата метода.

Рассмотрим тот же пример с асинхронной операцией:

func testFunctionWithEscapingClosure(closure:@escaping () -> Void) {
      DispatchQueue.main.async {
           closure()
      }
 }

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

 var completionHandlers: [() -> Void] = []
 func testFunctionWithEscapingClosure(closure: @escaping () -> Void) {
      completionHandlers.append(closure)
 }

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

В Swift 3 были добавлены экранирующие и не экранирующие закрытия для оптимизации компилятора. Вы можете искать преимущества nonescaping закрытие.

Я считаю этот сайт очень полезным в этом отношении. Простым объяснением будет:

Если замыкание передается в качестве аргумента функции и вызывается после возврата из функции, замыкание завершается.

Узнайте больше по ссылке, которую я прошел выше!:)

По умолчанию замыкания не экранируют. Для простого понимания вы можете рассматривать non_escaping замыкания как локальные замыкания (точно так же, как локальные переменные) и экранирование как глобальные замыкания (точно так же, как глобальные переменные). Это означает, что как только мы выйдем из тела метода, область действия non_escaping будет потеряна. Но в случае избежания закрытия память сохраняла закрытие в памяти.

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

Non_escaping закрытие: -

func add(num1: Int, num2: Int, completion: ((Int) -> (Void))) -> Int {
    DispatchQueue.global(qos: .background).async {
        print("Background")
        completion(num1 + num2) // Error: Closure use of non-escaping parameter 'completion' may allow it to escape
    }
    return num1
}

override func viewDidLoad() {
    super.viewDidLoad()
    let ans = add(num1: 12, num2: 22, completion: { (number) in
        print("Inside Closure")
        print(number)
    })
    print("Ans = \(ans)")
    initialSetup()
}

Поскольку это закрытие non_escaping, его область будет потеряна, как только мы выйдем из метода add. завершение (num1 + num2) никогда не вызовет.

Выход из закрытия: -

func add(num1: Int, num2: Int, completion: @escaping((Int) -> (Void))) -> Int {
    DispatchQueue.global(qos: .background).async {
        print("Background")
        completion(num1 + num2)
    }
    return num1
}

Даже если метод return (т.е. мы выйдем из области видимости метода), будет вызвано замыкание.enter code here

Swift 4.1

Из справочника по языку: атрибуты языка программирования Swift (Swift 4.1)

Apple объясняет атрибут escaping ясно.

Примените этот атрибут к типу параметра в объявлении метода или функции, чтобы указать, что значение параметра может быть сохранено для последующего выполнения. Это означает, что значение может пережить время жизни вызова. Параметры типа функции с атрибутом escape-типа требуют явного использования self. для свойств или методов. Для примера того, как использовать escape-атрибут, смотрите Escaping Closures

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

someFunctionWithEscapingClosure(_:) Функция принимает в качестве аргумента замыкание и добавляет его в массив, объявленный вне функции. Если вы не пометили параметр этой функции @escaping, вы получите ошибку во время компиляции.

Говорят, что замыкание экранирует функцию, когда замыкание передается в качестве аргумента функции, но вызывается после ее возврата. Когда вы объявляете функцию, которая принимает замыкание в качестве одного из своих параметров, вы можете написать @escaping перед типом параметра, чтобы указать, что закрытию разрешено экранировать.

Определение побега

Замыкания Swift являются ссылочными типами, что означает, что если вы указываете две переменные на одном и том же замыкании, они разделяют это замыкание - Swift просто запоминает, что на него полагаются две вещи, увеличивая счетчик ссылок.

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

Быстрый пример

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

      public func responseData(
    queue: DispatchQueue? = nil,
    completionHandler: @escaping (DataResponse<Data>) -> Void)
    -> Self
{
    ...

Дополнительная информация

По соображениям производительности Swift предполагает, что все замыкания не являются закрывающими, что означает, что они будут использоваться сразу внутри функции и не сохраняться, что, в свою очередь, означает, что Swift не затрагивает счетчик ссылок. Если это не так - если вы предпримете какие-либо меры для сохранения закрытия - тогда Swift заставит вас пометить его как @escaping, чтобы счетчик ссылок нужно было изменить.

[Функция и завершение]

неэкранированное против избегающего закрытия

non-escaping closure

@noescapeэто закрытие, которое передается в функцию и вызывается до того, как функция вернет

Хороший пример non-escaping closure является Array sort function - sorted(by: (Element, Element) -> Bool). Это закрытие вызывается при выполнении вычислений сортировки.

История: @noescapeбыл представлен вSwift 2-> устарело вSwift 3где стало по умолчанию, поэтому стоит отметить@escaping атрибут явно.

escaping closure

@escaping- это замыкание, которое передается в функцию и вызывается после возврата из функции. Функция владельца сохраняет это закрытие в свойстве и вызывает его, когда это необходимо. Хороший примерescaping closure это completion handlerв асинхронном режиме. Если вы не отметили свою функцию@escapingв этом случае вы получите ошибку времени компиляции. Ссылка на недвижимость вescaping closure требует, чтобы вы использовали self явно

Вот простой пример экранирования и отсутствия экранирования замыканий.
Замыкания без экранирования

      class NonEscapingClosure {

func performAddition() {
    print("Process3")
    addition(10, 10) { result in
        print("Process4")
        print(result)
    }
    print("Process5")
}

func addition(_ a: Int, _ b: Int, completion: (_ result: Int) -> Void) {
    print("Process1")
    completion(a + b)
    print("Process2")
}}

Создание экземпляра и вызов функции

      let instance = NonEscapingClosure()
    instance.performAddition()

Выход:

       Process3
 Process1
 Process4
 20
 Process2
 Process5

И выход из замыканий

      class EscapingClosure {

func performAddition() {
    print("Process4")
    addition(10, 10) { result in
        print(result)
    }
    print("Process5")
}

func addition(_ a: Int, _ b: Int, completion: @escaping (_ result: Int) -> Void) {
    print("Process1")
    let add = a + b
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        print("Process2")
        completion(add)
    }
    print("Process3")
}}

Создание экземпляра и вызов функции

      let instance = EscapingClosure()
    instance.performAddition()

Выход

       Process4
 Process1
 Process3
 Process5
 Process2
 20

Ускользающее замыкание — это то, которое мы хотим запустить в более поздний момент времени. По умолчанию все замыкания, которые мы определяем в Swift, выполняются немедленно. Это означает, что как только они встречаются во время выполнения, они выполняются и завершаются.

Экранирующие замыкания — это те, которые сохраняются и выполняются в какой-то момент в будущем, например, при завершении сетевого вызова.

Поэтому думайте о регулярных замыканиях как о замыканиях, которые выполняются немедленно. И экранирование замыканий как тех, которые сохраняются и выполняются в какой-то момент в будущем.

Другие вопросы по тегам