Operator defer został wprowadzony w Swift w celu bezpiecznego i gwarantowanego czyszczenia zasobów lub wykonania kodu w bloku wyjścia z zakresu, analogicznie do finally/using/RAII w innych językach.
Wielostopniowa inicjalizacja, praca z plikami, krzyżowe scenariusze wyjścia z funkcji — wymagają gwarantowanego zwolnienia zasobów lub wykonania logiki (zamknięcie pliku, odblokowanie mutexu, zwrócenie obiektu do puli, reset tymczasowego stanu). Do pojawienia się defer wszystko trzeba było robić ręcznie w każdym return.
defer gwarantuje wykonanie jego kodu przy wyjściu z bieżącego zakresu, niezależnie od tego, czy wyjście było normalne, czy przez throw. Można zadeklarować wiele defer — zostaną wykonane w odwrotnej kolejności deklaracji.
Przykład zwolnienia zasobu:
func processFile(path: String) throws { let file = try openFile(path) defer { file.close() } // Gwarantowane wywołanie nawet przy błędach // ... praca z file ... }
Kluczowe cechy:
Czy defer może przechwytywać zmienne przez referencje i jak to wpływa na zachowanie zamknięć?
Tak, defer przechwytuje wszystkie używane zmienne w momencie wywołania, według zasad przechwytywania closure (kopie dla wartości, referencje dla typów referencyjnych). Błędem będzie przechwycenie zmiennej zewnętrznej typu value, która może się zmienić w momencie wykonania defer.
Jak działa zagnieżdżony defer w pętla i funkcjach?
Jeśli defer jest zadeklarowany wewnątrz pętli, jest wykonywany przy każdym zakończeniu iteracji zakresu:
for _ in 1...3 { defer { print("Koniec iteracji") } print("Wewnątrz") }
Wynik:
Wewnątrz
Koniec iteracji
Wewnątrz
Koniec iteracji
Wewnątrz
Koniec iteracji
Czy defer może się nie wykonać?
Wykonanie kodu defer jest gwarantowane tylko wtedy, gdy zakres rzeczywiście jest opuszczany. Jednak jeśli proces zakończy się awaryjnie (fatalError, crash) lub zostanie wywołany exit, defer nie zostanie wykonany. Również defer nie działa na poziomie metod obiektów — tylko w zakresie funkcji/bloku.
W kodzie po otwarciu pliku i pracy z nim zwrot odbywał się przez różne return/throw, zapomniano w jednom miejscu zamknąć plik. W efekcie uchwycony uchwyt pliku wyciekł do systemu.
Zalety:
Wady:
Użycie defer dla gwarantowanego odblokowania mutexu, zwolnienia deskryptora pliku i resetowania flagi postępu:
func criticalSection() { lock() defer { unlock() } // ... praca ... }
Zalety:
Wady: