Escaping Closures (сбегающие замыкания)
Сбегающее замыкание (@escaping) - это ключевой термин, используемый для обозначения жизненного цикла замыкания, который передаётся в качестве аргумента функции. Добавляя к любому аргументу замыкания префикс @escaping, вы передаете сообщение вызывающему функцию, что это замыкание может «избежать» область вызова функции. Без префикса @escaping замыкание по умолчанию не является сбегающим, и его жизненный цикл заканчивается вместе с областью действия функции.
Ниже приведен пример несбегающего замыкания:
- Функция, которая измеряет время выполнения протекающего замыкания.
- Протекающее замыкание завершается при завершении функции. Никакое замыкание не выходит из области функции.
func benchmark(_ closure: () -> Void) {
let startTime = Date()
closure()
let endTime = Date()
let timeElapsed = endTime.timeIntervalSince(startTime)
print("Time elapsed: \(timeElapsed) s.")
}
Как замыкание может сбежать?
Термин @escaping может показаться вам непонятным, но фактическая реализация, позволяющая замыканию выжить в области действия вызывающей функции, очень проста. Чтобы выполняемое замыкание сохранилось после завершения функции, вы должны сохранить его в переменной, определенной вне функции.
Например, я создаю оболочку вокруг CLLocationManager, которая предоставляет новый метод для получения текущего местоположения в форме обратного вызова. Функция getCurrentLocation возвращается после вызова locationManager.requestLocation(), но замыкание не вызывается, пока мы не вернем местоположение пользователя из обратного вызова делегата. Итак, мы сохраняем его в переменной завершения completionHandler.
import Foundation
import CoreLocation
class MyLocationManager: NSObject, CLLocationManagerDelegate {
let locationManager: CLLocationManager
private var completionHandler: ((_ location: CLLocation) -> Void)? // <1>
override init() {
locationManager = CLLocationManager()
super.init()
locationManager.delegate = self
}
func getCurrentLocation(_ completion: @escaping (_ location: CLLocation) -> Void) { // <2>
completionHandler = completion // <3>
locationManager.requestLocation()
}
// MARK: - CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.first {
completionHandler?(location) // <4>
completionHandler = nil // <5>
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {}
}
<1> Переменная для хранения замыкания
<2> Здесь нужно указать @escaping, чтобы обозначить наше намерение. Несоблюдение этого правила приведет к следующей ошибке компиляции:
Назначение неcбегающего параметра ‘completion’ вместо @escaping замыкания.
<3> Мы помогаем замыканию выжить в области действия функции, сохраняя его вне области действия функции.
<4> После того, как мы получим данные о местоположении, мы вызываем замыкание с этой информацией.
<5> А потом освобождаем его от обязанностей.
Вложенное замыкание
Другой способ позволить замыканию сбежать - использовать это замыкание внутри другого сбегающего замыкания. В следующем примере мы передаем замыкание внутри очереди диспетчеризации.
func delay(_ closure: @escaping () -> Void) { // <1>
DispatchQueue.main.asyncAfter(wallDeadline: .now() + 3) {
closure() // <2>
}
}
<1> Вам нужно пометить замыкание как @escaping, поскольку <2> asyncAfter является @escaping функцией.
public func asyncAfter(wallDeadline: DispatchWallTime, qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = [], execute work: @escaping @convention(block) () -> Void)
Вы получите следующее сообщение об ошибке компиляции, если забудете о сбегающем замыкании:
Сбегающее замыкание захватывает несбегающий параметр "closure".
В этом случае нам не нужно знать базовую реализацию asyncAfter. Все, что нам нужно знать, это то, что DispatchQueue содержит ссылку на выполняемое замыкание и может пережить вызов DispatchQueue.main.asyncAfter. Все, что попадает в это замыкание, также захватывается и сохраняется в dispatch queue.
Зачем нам знать, является ли замыкание @escaping?
Тот факт, что замыкание @escaping хранится где-то еще, позволяет случайно создать цикл сильных ссылок. Таким образом, @escaping похож на знак предосторожности для вызывающего, чтобы он не терял бдительности при их использовании.
В качестве примера возьмем наш предыдущий класс MyLocationManager.
class DetailViewController: UIViewController {
let locationManager = MyLocationManager() // <1>
override func viewDidLoad() {
super.viewDidLoad()
locationManager.getCurrentLocation { (location) in
print("Get location: \(location)")
self.title = location.description // <2>
}
}
}
<1> DetailViewController владеет locationManager.
<2> Мы ссылаемся на self(DetailViewController) в выполняемом замыкании, которое захватывается замыканием. А сбегающее замыкание принадлежит MyLocationManager.
Это приводит к циклу сильных ссылок.
Цикл прервется, только если мы получим обновление местоположения и установим CompletionHandler в значении nil. Если нам не удастся определить местоположение, ничто не освободится из памяти, что приведет к утечке памяти.
// MARK: - CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.first {
completionHandler?(location)
completionHandler = nil // <1>
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {} // <2>
<1> Цикл сильных ссылок прервется, как только мы получим местоположение.
<2> В случае сбоя цикл сильных ссылок остается (поскольку здесь мы ничего не делаем).
Заключение
@escaping - это способ сообщить тем, кто использует нашу функцию, что параметр закрытия где-то хранится и может пережить область действия функции. Если вы видите какой-либо префикс @escaping, вы должны быть осторожны с тем, что вы передали в него замыкание, поскольку это может вызвать цикл сильных ссылок.
Ресурсы
Closures - swift.org