programowanieiOS/Mobile deweloper

Jak działają closures w Swift, czym różnią się od funkcji i jakie aspekty zarządzania pamięcią są związane z ich używaniem?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź.

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:

  • closure może przechwytywać wartości przez referencję lub przez wartość
  • escaping/non-escaping wpływa na cykl życia closure
  • closure jako przechowywane właściwości łatwo tworzą cykl zatrzymywania

Pytania pułapki.

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

Typowe błędy i antywzorce

  • Ustawienie closure jako właściwości klasy i odwoływanie się do self bez [weak self].
  • Deklarowanie closure zbyt dużymi — utrudnia zrozumienie i debugowanie.
  • Użycie escaping tam, gdzie można by się obyć bez non-escaping.

Przykład z życia

Negatywny przypadek

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.

Pozytywny przypadek

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.