İçeriğe geç / Skip to content / Zum Inhalt
Ahmet Balaman LogoAhmet Balaman

Swift Closures erklärt: $0, @escaping und [weak self]

Ahmet Balaman

7 Min. Lesezeit

SwiftClosuresEscapingWeak SelfSwiftUIiOS
Swift Closures erklärt: $0, @escaping und [weak self]

Eine Closure ist ein namenloser Codeblock, den Sie in einer Variablen speichern und einer Funktion als Argument übergeben können. In Swift sind sorted, map, filter, die Completion-Handler von Netzwerkaufrufen und jede Button-Aktion in SwiftUI Closures; wer die Sprache benutzt, kommt an ihnen nicht vorbei. Einsteigern macht dabei nicht das Konzept zu schaffen, sondern dass sich dasselbe auf sechs verschiedene Arten schreiben lässt. In diesem Beitrag gehen wir von der längsten Schreibweise zur kürzesten und danach zur Speicherseite: @escaping und [weak self].

Von der Funktion zur Closure

Funktion und Closure unterscheiden sich nur im Namen und in der Schreibweise:

func greet(name: String) -> String {
    return "Hallo, \(name)"
}

let greetClosure = { (name: String) -> String in
    return "Hallo, \(name)"
}

print(greet(name: "Ada"))      // Hallo, Ada
print(greetClosure("Ada"))     // Hallo, Ada

Bei der Closure wandern Parameter und Rückgabetyp in die geschweiften Klammern, und das Schlüsselwort in trennt Signatur und Rumpf. greetClosure hat den Typ (String) -> String. Beachten Sie, dass beim Aufruf kein Argument-Label geschrieben wird.

In sechs Schritten von der Langform zu $0

Kürzen wir dieselbe Sortierung Schritt für Schritt:

let numbers = [4, 1, 7, 3]

// 1. Langform
let s1 = numbers.sorted(by: { (a: Int, b: Int) -> Bool in
    return a < b
})

// 2. Typen ergeben sich aus dem Kontext
let s2 = numbers.sorted(by: { a, b in return a < b })

// 3. Ein einzelner Ausdruck braucht kein return
let s3 = numbers.sorted(by: { a, b in a < b })

// 4. Kurze Argumentnamen
let s4 = numbers.sorted(by: { $0 < $1 })

// 5. Trailing Closure
let s5 = numbers.sorted { $0 < $1 }

// 6. Der Operator selbst ist eine Funktion
let s6 = numbers.sorted(by: <)

Alle sechs liefern [1, 3, 4, 7]. Der Compiler weiß, dass sorted(by:) ein (Int, Int) -> Bool erwartet, daher müssen Sie die Typen nicht ausschreiben. $0 ist der erste Parameter, $1 der zweite.

Welche Variante wählen? Besteht der Rumpf aus einem kurzen Ausdruck, liest sich $0 gut. Umfasst er mehrere Zeilen oder sind Closures verschachtelt, geben Sie dem Parameter einen Namen; in verschachtelten Closures gehört $0 zur innersten, und schnell ist unklar, welchen Wert Sie meinen.

Trailing Closures

Ist der letzte Parameter einer Funktion eine Closure, darf sie außerhalb der runden Klammern stehen:

func repeatTask(times: Int, task: (Int) -> Void) {
    for index in 1...times {
        task(index)
    }
}

repeatTask(times: 3) { index in
    print("Durchlauf \(index)")
}

Deshalb wirkt SwiftUI-Code so aufgeräumt: VStack { … }, Button("Speichern") { … } und List { … } sind allesamt Trailing Closures.

Werte einfangen

Eine Closure fängt die Variablen aus ihrer Umgebung ein und hält sie am Leben, auch wenn dieser Gültigkeitsbereich längst beendet ist:

func makeCounter() -> () -> Int {
    var count = 0
    return {
        count += 1
        return count
    }
}

let next = makeCounter()
print(next())   // 1
print(next())   // 2

let another = makeCounter()
print(another())   // 1

Wenn makeCounter zurückkehrt, müsste count eigentlich verschwinden. Die zurückgegebene Closure hat die Variable jedoch eingefangen, also lebt sie weiter. another bekommt ein eigenes, getrenntes count.

Ein wichtiges Detail: Eine Closure fängt die Variable selbst ein, nicht ihren momentanen Wert. Um den Wert einzufrieren, verwenden Sie eine Capture List:

var level = 1
let printLive = { print("Level: \(level)") }
let printFrozen = { [level] in print("Level: \(level)") }

level = 5
printLive()     // Level: 5
printFrozen()   // Level: 1

Closures sind Referenztypen. Weisen Sie eine Closure zwei Variablen zu, teilen sich beide denselben eingefangenen Zustand. Den Unterschied zwischen Wert- und Referenztypen erkläre ich ausführlich in struct vs. class in Swift.

@escaping: eine Closure, die die Funktion überlebt

Standardmäßig ist ein Closure-Parameter non-escaping: Er wird aufgerufen, bevor die Funktion zurückkehrt, und ist danach erledigt. Wird die Closure erst nach dem Ende der Funktion aufgerufen, also in einer Eigenschaft gespeichert oder an asynchrone Arbeit übergeben, muss sie mit @escaping markiert sein:

import Foundation

func fetchGreeting(completion: @escaping (String) -> Void) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        completion("Hallo")
    }
}

fetchGreeting { text in
    print(text)   // nach 1 Sekunde: Hallo
}

Entfernen Sie @escaping, meldet der Compiler Escaping closure captures non-escaping parameter 'completion'. Die Markierung ist keine Formalie, sondern ein Hinweis: Diese Closure kann das Eingefangene lange festhalten, denken Sie an den Speicher. Aus demselben Grund verlangt Swift in escaping Closures innerhalb einer Klasse ein ausdrückliches self.; fehlt es, erhalten Sie reference to property … in closure requires explicit use of 'self' to make capture semantics explicit.

Retain Cycles und [weak self]

Das Problem entsteht so: Ein Objekt speichert eine Closure in einer Eigenschaft, und die Closure fängt self ein. Beide halten sich gegenseitig mit starken Referenzen, also wird keines je freigegeben.

final class Stopwatch {
    var seconds = 0
    var onTick: (() -> Void)?

    deinit {
        print("Stopwatch freigegeben")
    }

    func start() {
        onTick = {
            self.seconds += 1      // self -> onTick -> self
        }
    }
}

var stopwatch: Stopwatch? = Stopwatch()
stopwatch?.start()
stopwatch = nil     // die deinit-Meldung erscheint nie

Die Lösung: self schwach einfangen.

func start() {
    onTick = { [weak self] in
        guard let self else { return }
        self.seconds += 1
    }
}

Mit [weak self] wird self in der Closure zum Optional und ist nil, sobald das Objekt verschwunden ist. Das Muster guard let self else { return } ist der sauberste Weg, es auszupacken; die Logik ist dieselbe wie im Beitrag zu guard let. Nach dieser Änderung gibt stopwatch = nil die deinit-Meldung aus.

[unowned self] ist ebenfalls möglich, doch läuft die Closure, nachdem das Objekt freigegeben wurde, stürzt die App ab. Solange Sie nicht sicher sind, dass die Closure das Objekt nicht überleben kann, nehmen Sie weak.

[weak self] gehört nicht in jede Closure. Non-escaping Closures wie bei map, filter und sorted enden zusammen mit der Funktion und können keinen Zyklus bilden.

map, filter, sorted und reduce

Am meisten Freude machen Closures bei Collections:

struct Student {
    let name: String
    let grade: Int
}

let students = [
    Student(name: "Ada", grade: 85),
    Student(name: "Linus", grade: 42),
    Student(name: "Grace", grade: 97),
    Student(name: "Alan", grade: 68)
]

let passedNames = students
    .filter { $0.grade >= 50 }
    .sorted { $0.grade > $1.grade }
    .map { $0.name }

print(passedNames)   // ["Grace", "Ada", "Alan"]

let total = students.reduce(0) { sum, student in sum + student.grade }
let average = Double(total) / Double(students.count)
print(average)       // 73.0

filter behält die Elemente, die eine Bedingung erfüllen, sorted ordnet sie, map wandelt jedes Element in etwas anderes um, und reduce faltet alles zu einem einzigen Wert. Bei reduce bedeuten die beiden Parameter Unterschiedliches, daher sind Namen lesbarer als $0 + $1.grade. Wenn Sie nur eine Eigenschaft herausziehen, ist ein Key Path noch kürzer: students.map(\.grade).

Button-Aktionen in SwiftUI

import SwiftUI

struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack(spacing: 16) {
            Text("\(count)")
                .font(.largeTitle)

            Button("Erhöhen") {
                count += 1
            }

            Button(action: { count = 0 }) {
                Label("Zurücksetzen", systemImage: "arrow.counterclockwise")
            }
        }
    }
}

Der erste Button erhält seine Aktion als Trailing Closure. Der zweite hat zwei Closures: action und label, das die Darstellung erzeugt. Weil SwiftUI-Views Structs sind, schreiben Sie hier kein [weak self]; der Versuch endet mit 'weak' may only be applied to class and class-bound protocol types. Das Risiko eines Retain Cycles liegt nicht in der View, sondern in den klassenbasierten Modellobjekten, die die View verwendet. Wie diese Struktur von Grund auf entsteht, lesen Sie in Erste App mit SwiftUI entwickeln.

Ein realistisches Szenario: das Modell hinter einem Suchscreen

import Foundation

final class SearchViewModel {
    private(set) var results: [String] = []
    var onResultsChanged: (([String]) -> Void)?

    private let allItems = ["Swift", "SwiftUI", "Xcode", "Flutter", "Dart"]

    func search(_ query: String) {
        loadItems(matching: query) { [weak self] found in
            guard let self else { return }
            self.results = found
            self.onResultsChanged?(found)
        }
    }

    private func loadItems(matching query: String,
                           completion: @escaping ([String]) -> Void) {
        let items = allItems
        DispatchQueue.global().async {
            let found = items.filter { $0.localizedCaseInsensitiveContains(query) }
            DispatchQueue.main.async {
                completion(found)
            }
        }
    }
}

let viewModel = SearchViewModel()
viewModel.onResultsChanged = { print("Ergebnisse:", $0) }
viewModel.search("swift")   // Ergebnisse: ["Swift", "SwiftUI"]

In dieser kleinen Klasse steckt alles aus dem Beitrag. completion ist @escaping, weil es an eine Hintergrund-Queue übergeben wird. Der Nutzer kann den Screen schließen, bevor Ergebnisse eintreffen; dank [weak self] bleibt das Modell nicht im Speicher hängen, die Closure steigt still aus. Die Closure in filter ist non-escaping und bleibt mit $0 kurz. Dass die Hintergrund-Closure eine Kopie von items statt self erhält, ist ebenfalls Absicht: Ein Array ist ein Werttyp und lässt sich sicher zwischen Queues weiterreichen.

Wann verwenden – und wann nicht?

Eine Closure ist das richtige Werkzeug, um Verhalten als Parameter zu übergeben (sorted, filter), für kurze Reaktionen auf Ereignisse (Button-Aktion) und für ein Objekt, das genau ein Ereignis nach außen meldet (onResultsChanged).

Schauen Sie sich eine Alternative an, wenn:

  • asynchrone Schritte anfangen, sich zu verschachteln, also Completion in Completion. async/await ist dann deutlich lesbarer; den Umstieg beschreibe ich im Leitfaden zu async/await.
  • der Rumpf der Closure auf Dutzende Zeilen angewachsen ist. Verschieben Sie ihn in eine benannte Methode und übergeben Sie diese als Referenz: Button("Hinzufügen", action: addItem).
  • ein Objekt drei oder vier verschiedene Ereignisse meldet. Ein Protocol (Delegate) ist ordentlicher als eine Handvoll einzelner Closure-Eigenschaften.

Häufige Fehler

1. @escaping vergessen

Symptom: Escaping closure captures non-escaping parameter 'completion' oder, beim Zuweisen der Closure an eine Eigenschaft, assigning non-escaping parameter … to an '@escaping' closure. Lösung: @escaping vor den Parametertyp setzen.

2. self in einer gespeicherten Closure stark einfangen

Symptom: kein Compilerfehler, aber deinit läuft nie, obwohl der Screen geschlossen wurde, und Timer sowie Listener arbeiten im Hintergrund weiter. Lösung: [weak self] plus guard let self else { return }. Ein vorübergehendes deinit { print(…) } in der verdächtigen Klasse ist die schnellste Diagnose.

3. Annehmen, der Wert sei eingefroren

Symptom: Die Closure gibt den Wert zum Zeitpunkt der Ausführung aus, nicht den zum Zeitpunkt der Definition. Eine Closure fängt die Variable ein, nicht den Wert. Lösung: eine Capture List wie { [level] in … } verwenden.

4. $0 in verschachtelten Closures

Symptom: Sie glauben, $0 meine das äußere Element, tatsächlich ist es der Parameter der inneren Closure; das Ergebnis ist stillschweigend falsch oder es gibt einen Typfehler. Lösung: dem Parameter der äußeren Closure einen Namen geben. Für let matrix = [[1, 2], [3, 4]] ist bei matrix.map { row in row.filter { $0 > row[0] } } row das äußere und $0 das innere Element, ohne Verwechslung.

Häufig gestellte Fragen

Was ist der Unterschied zwischen einer Closure und einer Funktion?

Funktionen sind im Grunde Closures mit Namen. Eine Closure ist namenlos, wird an Ort und Stelle geschrieben und kann Variablen aus ihrer Umgebung einfangen; der Typ hat in beiden Fällen die Form (Parameter) -> Rückgabetyp.

Was bedeutet $0?

Es ist der automatische Name des ersten Closure-Parameters; der zweite heißt $1, der dritte $2. Sie können ihn verwenden, wenn Sie die Parameter nicht mit in benennen, und er eignet sich für kurze Closures mit einem einzigen Ausdruck.

Sollte ich in jeder Closure [weak self] schreiben?

Nein. Nötig ist es nur, wenn die Closure gespeichert wird oder lange lebt und self eine Klasseninstanz ist. In non-escaping Closures wie bei map und filter sowie in SwiftUI-Views, die Structs sind, ist es überflüssig.

Wann ist @escaping erforderlich?

Wenn die Closure aufgerufen wird, nachdem die Funktion, der sie übergeben wurde, zurückgekehrt ist: Sie wird in einer Eigenschaft oder einem Array gespeichert oder an asynchrone Arbeit übergeben. Closures, die innerhalb der Funktion aufgerufen und abgeschlossen werden, brauchen es nicht.

Kommentare