Что такое @escaping в Swift замыканиях?

Новости, Туториалы

Что такое @escaping в Swift замыканиях?

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

Оригинал статьи

Комментарии

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: