defer — konstrukcja sterująca w Swift, która planuje wykonanie bloku kodu na moment wyjścia z bieżącego zakresu widoczności (scope). Blok defer jest wykonywany niezależnie od sposobu zakończenia — return, break, throw, fatalError lub normalne zakończenie. Według Swift Language Guide (2025), przy kilku defer w jednym scope są one wykonywane w odwrotnej kolejności deklaracji — ostatni zadeklarowany jest wykonywany jako pierwszy (LIFO). To czyni defer niezastąpionym dla gwarantowanego czyszczenia zasobów: zamykania deskryptorów plików, zwalniania blokad, zwalniania tymczasowych wskaźników bez ryzyka pominięcia cleanup przy wcześniejszym wyjściu.
Najważniejsze
defer — to konstrukcja sterująca języka Swift, wprowadzona w Swift 2.0 (2015), która opóźnia wykonanie swojego bloku do momentu zakończenia bieżącego scope. Kluczowa cecha: defer gwarantuje wykonanie swojego ciała niezależnie od tego, jak dokładnie kończy się zakres — pomyślnie (return), z błędem (throw), przedwcześnie (break, continue) lub fatalnie (fatalError, precondition).
Składniowo defer wygląda jak defer { /* kod */ } i może znajdować się w dowolnym miejscu wewnątrz scope. Kompilator Swift gwarantuje, że kod wewnątrz defer zostanie wykonany, nawet jeśli między deklaracją defer a końcem scope wystąpi wyjątek lub return. To zasadniczo odróżnia defer od zwykłego kodu umieszczonego na końcu funkcji, który może zostać pominięty przy wcześniejszym wyjściu.
Według artykułu Chrisa Lattnera (Twórca Swift, 2015), defer był inspirowany analogicznymi konstrukcjami w innych językach — defer w Go, finally w Java/Python, scope guard w C++ — ale z ważną różnicą: w Swift defer jest wykonywany na końcu scope, a nie bezpośrednio po bloku try-catch. Daje to bardziej przewidywalne zachowanie dla cleanup w funkcjach z wieloma punktami wyjścia.
Używaj defer do symetrycznego zarządzania zasobami: otwarcie pliku → defer { close }, ustawienie blokady → defer { unlock }. Taki wzorzec gwarantuje, że zwolnienie zasobu nie zostanie pominięte w żadnych okolicznościach.
Gdy w jednym scope zadeklarowano kilka defer, są one wykonywane w odwrotnej kolejności deklaracji (LIFO — Last In, First Out). Oznacza to, że ostatni zadeklarowany defer wykona się jako pierwszy, a pierwszy — jako ostatni:
func exampleDeferOrder() {
defer { print("Pierwszy defer") }
defer { print("Drugi defer") }
defer { print("Trzeci defer") }
print("Ciało funkcji")
}
// Wynik:
// Ciało funkcji
// Trzeci defer
// Drugi defer
// Pierwszy defer
Kolejność LIFO jest ważna dla poprawnego zarządzania zagnieżdżonymi zasobami. Jeśli najpierw otwierany jest plik A, następnie plik B, zwalniać je należy w odwrotnej kolejności: najpierw B, potem A. Z defer dzieje się to automatycznie — zadeklaruj defer zaraz po otwarciu każdego zasobu, a kolejność czyszczenia będzie prawidłowa niezależnie od liczby punktów wyjścia z funkcji.
Według Swift by Sundell (2024), ta cecha czyni defer idealnym dla zagnieżdżonych blokad i transakcji: przechwycenie blokady → defer { unlock } → przechwycenie następnej → defer { unlock }. LIFO gwarantuje, że blokady są zwalniane w kolejności odwrotnej do przechwytywania, zapobiegając zakleszczeniom.
Główne zastosowanie defer — gwarantowane czyszczenie zasobów. Rozważmy pracę z systemem plików. Otwarcie pliku przez FileHandle wymaga jawnego zamknięcia — defer gwarantuje, że close zostanie wywołane w każdym scenariuszu:
func readFile(path: String) throws -> String {
let handle = try FileHandle(forReadingFrom: URL(fileURLWithPath: path))
defer { try? handle.close() }
let data = try handle.readToEnd()
guard let data else { throw FileError.empty() }
return String(data: data, encoding: .utf8) ?? ""
// handle.close() zostanie wywołane nawet przy throw lub return
}
Inny typowy scenariusz — UI-animacje z flagą ładowania. Przed rozpoczęciem ładowania ustawiana jest flaga isLoading = true, a defer zmienia ją na false przy wyjściu z funkcji, niezależnie od sukcesu lub błędu zapytania. Zapobiega to sytuacji, w której flaga pozostaje true z powodu nieobsłużonego błędu i blokuje interfejs na zawsze.
Według Bitbucket Engineering Blog (2024), defer jest również używany do profilowania: na początku funkcji można zapisać czas, a w defer — obliczyć i wyświetlić różnicę. Daje to dokładne pomiary wydajności wszystkich ścieżek wykonania, włącznie z błędnymi.
defer skutecznie łączy się z funkcjami throws. Gdy funkcja może wyrzucić błąd na dowolnym etapie, defer gwarantuje cleanup bez duplikowania kodu w każdym bloku catch lub wczesnym wyjściu guard:
func processTransaction() throws {
let db = try openDatabase()
defer { closeDatabase(db) }
let user = try fetchUser(from: db)
defer { logAudit(user) }
let result = try performPayment(user)
sendNotification(result)
// closeDatabase(db) i logAudit(user) zostaną wywołane
// przy każdym throw lub return
}
Ważne: defer jest wykonywany przed przekazaniem sterowania z bloku catch, ale po wystąpieniu błędu. Jeśli w defer zostanie wyrzucony błąd, Swift nie pozwala na użycie try wewnątrz defer bezpośrednio — wymagane jest try? lub try!. Według Apple Documentation, Swift nie pozwala, aby błąd „wyleciał" z defer, ponieważ naruszyłoby to gwarancję wykonania bloku.
Umieszczaj defer zaraz po przechwyceniu zasobu. Jest to zgodne z zasadą bliskości: czytelnik widzi przechwycenie i zwolnienie obok siebie, co zwiększa niezawodność kodu i upraszcza przegląd kodu.
defer jest wykonywany przy wyjściu z zakresu, w którym został zadeklarowany. Jeśli defer jest zadeklarowany wewnątrz bloku do, jest wykonywany przy wyjściu z tego bloku, a nie z funkcji zewnętrznej. Jeśli wewnątrz pętli for — przy każdej iteracji:
func scopeExample() {
print("start")
do {
defer { print("defer w do-bloku") }
print("inside do")
}
// "defer w do-bloku" wypisuje tutaj
print("after do")
}
// Wynik: start, inside do, defer w do-bloku, after do
for i in 1...3 {
defer { print("end iteration \(i)") }
print("iteration \(i)")
}
// Wynik: iteracja 1, koniec iteracji 1, iteracja 2, koniec iteracji 2, ...
Zmienne przechwycone przez defer są odczytywane w momencie wyjścia z zakresu, a nie w momencie deklaracji defer. Jeśli zmienna zmienia się między deklaracją defer a końcem zakresu, defer zobaczy ostatnią wartość. To ważna różnica w stosunku do domknięć, gdzie przechwycenie następuje w momencie utworzenia. Bądź ostrożny: zmiany zmiennej po deklaracji defer wpłyną na jego wykonanie.
Pierwszy błąd — założenie o kolejności wykonania innej niż LIFO. Jeśli kolejność cleanup jest ważna, a defer są zadeklarowane w nieprawidłowej kolejności, zasoby mogą być zwalniane z naruszeniem zależności. Rozwiązanie: deklaruj defer zaraz po przechwyceniu każdego zasobu. Drugi zasób otwarty → defer { close second } zanim pierwszy zostanie zamknięty.
Drugi błąd — używanie defer do logiki niezwiązanej z czyszczeniem. defer jest przeznaczony do gwarantowanego cleanup, a nie do głównego przepływu sterowania. Jeśli kod w defer wpływa na wartość zwracaną, to prawie zawsze błąd. defer nie może zmienić wartości return funkcji (w przeciwieństwie do Java finally, gdzie return w finally nadpisuje oryginalny return).
Trzeci błąd — wyrzucanie błędu z defer. Swift zabrania try wewnątrz defer, jeśli błąd może rozprzestrzenić się na zewnątrz. Używaj try? lub try! dla operacji, które mogą wyrzucić błąd, lub owijaj je w osobną funkcję bez throws. Według O'Reilly „Swift in Depth" (2025), dobrą praktyką jest uczynienie funkcji cleanup nie rzucających wyjątków (non-throwing) lub obsługiwanie błędów wewnątrz defer.
Często zadawane pytania
defer — konstrukcja sterująca Swift, opóźniająca wykonanie bloku do wyjścia z bieżącego zakresu widoczności. Blok jest wykonywany zawsze — przy return, throw, break lub normalnym zakończeniu. Używany do gwarantowanego czyszczenia zasobów: zamykania plików, zwalniania blokad.
W odwrotnej kolejności deklaracji (LIFO) — ostatni zadeklarowany defer jest wykonywany jako pierwszy. Gwarantuje to prawidłowy cleanup zagnieżdżonych zasobów: jeśli zasób B został otwarty po A, zostanie zamknięty przed A, co zapobiega zależnościom od już zwolnionych zasobów.
Nie bezpośrednio — Swift zabrania propagowania błędu z defer. Używaj try? lub try! dla operacji, które mogą wyrzucić błąd. Najlepszą praktyką jest tworzenie funkcji cleanup jako non-throwing lub obsługiwanie błędów wewnątrz defer bez propagowania na zewnątrz.
defer jest powiązany z zakresem i wykonywany przy każdym wyjściu, w tym return, throw i break. finally (w innych językach) jest powiązany z try-catch i wykonywany tylko przy wystąpieniu try. W Swift nie ma finally — defer w pełni pokrywa ten scenariusz i działa dla dowolnego zakresu, a nie tylko dla obsługi błędów.
Tak, defer odczytuje zmienne w momencie wyjścia z zakresu, a nie w momencie deklaracji. Jeśli zmienna zmienia się po deklaracji defer, blok defer zobaczy ostatnią wartość. Różni się to od zwykłych domknięć, gdzie przechwycenie jest ustalane w momencie utworzenia.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również