Historia pytania:
Closures, jako koncepcja, pochodzą z języków funkcyjnych i pozwalają na przekazywanie bloków kodu jako wartości. W Swift closures (analogiczne do lambd z innych języków) pojawiły się od samego początku. Dzięki temu można elegancko realizować asynchroniczne wywołania, delegować działania, sortować kolekcje, filtrować itd.
Problem:
Closures przechwytują zmienne z otaczającego kontekstu. Jeśli wśród tych zmiennych znajdą się referencje do obiektów klasy, może powstać cykl zatrzymywania (retain cycle), szczególnie jeśli closure jest przechowywane jako właściwość tej klasy. Ważne jest również, aby rozumieć różnicę między escaping a non-escaping closures, a także być świadomym, jak działa przechwytywanie wartości.
Rozwiązanie:
Swift implementuje closures jako samodzielne obiekty, mogą one przechwytywać kontekst, a właściciel closure powinien być ostrożny w architekturze.
Przykład kodu:
class Performer { var onComplete: (() -> Void)? func doWork() { onComplete = { print("Zakończono pracę!") } } } // Przechwytywanie self w closure class Test { var value = 0 func start() { DispatchQueue.global().async { [weak self] in self?.value = 1 } } }
Kluczowe cechy:
Czy jeśli closure nie przechwytuje self wyraźnie, może wystąpić cykl zatrzymywania?
Tak, jeśli closure jest przechwytywane jako strong property klasy i odwołuje się do self w środku, powstaje cykl zatrzymywania, nawet jeśli wyraźnie nie piszesz self. Użyj [weak self]/[unowned self] lub zrób closure lokalne, jeśli to możliwe.
Czym różni się escaping od non-escaping closure?
Escaping closure może być wywołane po zakończeniu funkcji i zwykle jest używane do operacji asynchronicznych. Non-escaping jest wykonywane przed zakończeniem wykonania funkcji. Przy escaping kolejność przechwytywania self jest inna.
Przykład kodu:
func asyncWork(completion: @escaping () -> Void) { DispatchQueue.global().async { completion() } }
Czy closure może zmieniać wartości przechwyconych zmiennych?
Tak, jeśli closure przechwycił zmienną zadeklarowaną jako var, może zmieniać jej wartość (dla typów wartości). Dla klasy będzie to referencja i właściwości można zmieniać zawsze.
Przykład kodu:
var value = 1 let closure = { value += 1 } closure() print(value) // 2
ViewController przechowuje closure jako właściwość (np. completionHandler) i bezpośrednio odwołuje się do self. W rezultacie powstaje cykl zatrzymywania: ViewController => closure => ViewController. Wyłączenie ekranu nie zwalnia pamięci.
Zalety: Kod jest zwarty i krótki wizualnie.
Wady: Wycieki pamięci, ViewController "zawiesza się" w pamięci, potencjalne błędy.
Użycie [weak self] lub [unowned self] wewnątrz closure, lub closure jest przechowywane nie dłużej niż cykl życia obiektu. Przegląd takich miejsc podczas code-review.
Zalety: Poprawne zwalnianie zasobów, brak nieprzewidywalnych wycieków.
Wady: [weak self] wymaga ostrożności podczas dereferencji, mogą wystąpić niejawne awarie przy niewłaściwym użyciu.