Экранирование замыканий в Свифте
Я новичок в Свифте, и я читал руководство, когда наткнулся на спасение замыканий. Я не получил описание руководства вообще. Может, кто-нибудь объяснит мне, что такое закрывающиеся замыкания в 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, выполняются немедленно. Это означает, что как только они встречаются во время выполнения, они выполняются и завершаются.
Экранирующие замыкания — это те, которые сохраняются и выполняются в какой-то момент в будущем, например, при завершении сетевого вызова.
Поэтому думайте о регулярных замыканиях как о замыканиях, которые выполняются немедленно. И экранирование замыканий как тех, которые сохраняются и выполняются в какой-то момент в будущем.