defer — co to jest, składnia i kolejność wykonywania

Autor: IT Sectr Opublikowano: 2026-06-20 Czas czytania: 8 min

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 — blok kodu wykonywany przy wyjściu z zakresu widoczności, niezależnie od przyczyny wyjścia (return, throw, break)
  • Kolejność LIFO: kilka defer wykonuje się od dołu do góry — ostatni zadeklarowany uruchamia się jako pierwszy
  • Czyszczenie zasobów — główne zastosowanie: zamykanie plików, zwalnianie blokad, kończenie animacji
  • Definicja zmiennych: defer widzi zmienne w momencie wyjścia, a nie w momencie deklaracji
  • Nie zastępuje do-catch do obsługi błędów — defer odpowiada za cleanup, a nie za kontrolę przepływu

Co to jest defer?

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.

Kolejność wykonywania wielu defer

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:

swift
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.

Defer do czyszczenia zasobów

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:

swift
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 i obsługa błędów

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:

swift
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.

Zasady zakresu widoczności

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:

swift
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.

Typowe błędy z defer

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

Co to jest defer w Swift?

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 jakiej kolejności wykonywanych jest kilka defer?

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.

Czy można wyrzucić błąd z defer?

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.

Jaka jest różnica między defer a do-catch-finally?

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.

Czy defer widzi zmiany zmiennych po swojej deklaracji?

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

  • defer — blok finalizujący, wykonywany przy wyjściu z zakresu niezależnie od przyczyny (return, throw, break, normalne zakończenie)
  • Kolejność LIFO — kilka defer wykonuje się od dołu do góry, ostatni zadeklarowany — pierwszy wykonany
  • Czyszczenie zasobów — główne zastosowanie: zamykanie plików, zwalnianie blokad, zwalnianie wskaźników, zatrzymywanie animacji
  • Zgodność z throws: defer jest wykonywany po błędzie, ale przed wyjściem z bloku catch; błąd z defer nie propaguje się
  • Scope: defer jest wykonywany przy wyjściu z tego zakresu, w którym został zadeklarowany — blok do, pętla, funkcja
  • Przechwytywanie zmiennych: defer odczytuje wartości w momencie wyjścia, a nie w momencie deklaracji — uważaj na mutacje
  • Najlepsza praktyka: deklaruj defer zaraz po przechwyceniu zasobu, nie używaj defer do logiki biznesowej, twórz funkcje cleanup jako non-throwing

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.

Omów projekt

Przeczytaj również