CodeGym /Kurse /Swift SELF /`self` in escaping closures: wann und warum `self.` notwe...

`self` in escaping closures: wann und warum `self.` notwendig ist

Swift SELF
Level 38, Lektion 2
Verfügbar

1. Warum der Compiler self. verlangt

Wenn Sie zum ersten Mal so etwas schreiben wie onFinish = { cleanup() }, möchte man, dass Swift einfach versteht, dass cleanup() eine Methode des aktuellen Objekts ist. Und oft versteht es das auch ... bis die Closure zu einer escaping-Closure wird. In diesem Moment wacht der Compiler plötzlich auf und sagt: „Füge self. hinzu.“ Das klingt pedantisch, ist aber in Wahrheit eine Methode, um Sie auf etwas Wichtiges aufmerksam zu machen: Eine Closure kann ein Objekt länger im Speicher halten, als Sie erwarten.

Beginnen wir mit einem möglichst einfachen Beispiel, ohne unsere App — nur um das Symptom zu sehen.

import Foundation

class Greeter {
    var name: String = "Alex"

    func sayHelloNow(_ work: () -> Void) {
        work()
    }

    func run() {
        sayHelloNow {
            print("Hello, \\(name)!") // bei non-escaping ist das meistens ok
        }
    }
}

Hier ist work non-escaping (standardmäßig), das heißt, der Compiler geht von der Idee aus: „Die Closure überlebt sayHelloNow nicht“. Deshalb ist er hier nicht so streng.

Ändern wir die Situation nun leicht: Wir speichern die Closure für später.

import Foundation

class Greeter {
    var name: String = "Alex"
    var stored: (() -> Void)?

    func storeForLater(_ work: @escaping () -> Void) {
        stored = work
    }

    func run() {
        storeForLater {
            print("Hello, \\(self.name)!") // self. ist hier kein Zufall mehr
        }
    }
}

Achten Sie darauf: @escaping steht hier nicht für „Asynchronität“ und nicht für Threads, sondern für die Lebensdauer: Die Closure kann später aufgerufen werden, also kann sie das festhalten, was sie eingefangen hat. Genau deshalb wird die Regel „In escaping Closures muss self sichtbar sein“ historisch als Schutz vor unbemerkten Retain-Zyklen verwendet.

2. Was die Compiler-Meldung bedeutet

Die Meldung des Compilers sieht gewöhnlich ungefähr so aus: “reference to property 'x' in closure requires explicit use of 'self' to make capture semantics explicit”. Ohne akademische Umwege übersetzt heißt das: „Ich sehe, dass Sie innerhalb einer escaping Closure eine Eigenschaft oder Methode des Objekts verwenden. Stellen Sie sicher, dass Sie das Objekt bewusst einfangen, denn das kann seine Lebensdauer beeinflussen.“

Wichtig ist eine Feinheit: Der Compiler verbietet nicht, self zu capturen. Er verbietet nur, das zufällig und unbemerkt zu tun. Im non-escaping-Kontext ist „zufällig“ noch halbwegs harmlos: Die Closure lebt kurz, und die Chance, self „für immer“ festzuhalten, ist gering. Im escaping-Kontext ist diese Chance sehr real.

Um die Idee zu spüren, stellen Sie sich vor, self sei eine Katze. In einer non-escaping Closure streicheln Sie die Katze und gehen wieder. In einer escaping Closure streicheln Sie die Katze, nehmen sie dann aus Versehen mit nach Hause und vergessen, sie zurückzubringen. Genau das ist self.: ein Schild mit der Aufschrift „Ich nehme die Katze wirklich mit“.

Zeigen wir ein typisches Beispiel, bei dem ohne self. ein Fehler entstünde (im Kommentar).

import Foundation

class Counter {
    var value: Int = 0
    var stored: (() -> Void)?

    func setup() {
        stored = {
            self.value += 1
            print(self.value) // 1
        }
    }
}

Wenn Sie value += 1 ohne self. geschrieben hätten, würde der Compiler im escaping-Kontext Sie bitten, den Capture explizit zu machen. Diese Regel wird genau als „die Capture-Semantik explizit machen“ beschrieben.

3. Wie man den Capture explizit macht: self. und capture list [self]

Jetzt die gute Nachricht: Die Anforderung „mach den Capture explizit“ kann man nicht nur mit dem Präfix self. vor jedem Zugriff erfüllen. In Swift gibt es einen zweiten Weg: das Objekt explizit zu capturen in der capture list — also in den eckigen Klammern vor in.

Vergleichen wir beide Stile an demselben Beispiel.

Beispiel (Weg 1 — viel self.):

import Foundation

class Printer {
    var prefix: String = ">>"
    var stored: (() -> Void)?

    func bind() {
        stored = {
            print("\\(self.prefix) one")
            print("\\(self.prefix) two")
        }
    }
}

Beispiel (Weg 2 — capture list [self]):

import Foundation

class Printer {
    var prefix: String = ">>"
    var stored: (() -> Void)?

    func bind() {
        stored = { [self] in
            print("\\(prefix) one")
            print("\\(prefix) two")
        }
    }
}

Die Bedeutung beider Varianten ist dieselbe: Die Closure fängt das Objekt ein. Der Unterschied liegt darin, wo Sie das sichtbar machen. Im ersten Fall bei jeder Verwendung. Im zweiten einmal am Anfang, und der Code innerhalb wirkt sauberer.

Wichtig: [self] ist ein expliziter starker Capture. Wenn also stored innerhalb desselben Objekts gespeichert wird, können Sie leicht einen retain cycle bauen. Darüber sprechen wir im nächsten Abschnitt. Merken wir uns bis dahin: Die Compiler-Regel lautet nicht „schreiben Sie self., weil man das eben so macht“, sondern „machen Sie dem Leser klar, dass self capturt wurde“.

4. ARC und retain cycle: Objekt → Closure → Objekt

Verbinden wir das nun mit dem, was Sie bereits über ARC wissen. Wir haben ein Objekt, und es hat die Eigenschaft stored, die eine Closure hält. Diese Eigenschaft ist eine normale starke Eigenschaft, also wird die Closure stark gehalten. Und die Closure wiederum kann self stark halten (ebenfalls stark, wenn wir nicht weak/unowned verwenden). Und schon haben wir einen Kreis.

Schemahaft sieht das so aus:

flowchart LR
    A["Objekt (self)"] -->|strong| B["stored: (() -> Void)?"]
    B -->|strong| C["Closure"]
    C -->|captures strong| A

Wenn sich dieser Kreis schließt, kann ARC den reference count nicht auf null senken, weil die „letzte starke Referenz“ immer innerhalb des Kreises liegt.

Schauen wir uns ein Beispiel ganz ehrlich an: Wir fügen deinit hinzu, um zu sehen, ob er aufgerufen wird.

import Foundation

class Leaky {
    var stored: (() -> Void)?

    func bind() {
        stored = { [self] in
            print("Doing work")
        }
    }

    deinit {
        print("Leaky deinit") // wenn ein Zyklus existiert, sehen Sie das nicht
    }
}

Dieser Code kompiliert an sich. Wenn Sie aber ein Objekt erstellen, bind() aufrufen und dann alle externen Referenzen entfernen — wird deinit möglicherweise nicht aufgerufen, weil das Objekt sich über die Closure selbst festhält. Und genau hier versucht der Compiler mit seinem „setze self. davor“, die Situation hervorzuheben: „Vorsicht, du capturst gerade ein Objekt in einer escaping Closure.“

5. Beispiel: das Ereignis onBookAdded in einer Mini-Bibliothek

Damit wir nicht in der Welt abstrakter Counter und Printer bleiben, fügen wir einen kleinen Teil zu unserer Konsolenanwendung „Mini-Bibliothek“ hinzu. Wir bauen keine Schichten- oder Repository-Architektur (das kommt später im Kurs), sondern modellieren nur die Situation: Wir haben eine Library, in der man ein Buch hinzufügen kann, und einen Callback onBookAdded, der „später“ aufgerufen wird (also in einer Eigenschaft gespeichert wird).

Beginnen wir mit einfachen Typen.

import Foundation

struct Book {
    let title: String
}

class Library {
    var onBookAdded: ((Book) -> Void)?

    func add(_ book: Book) {
        onBookAdded?(book)
    }
}

Stellen wir uns nun einen Controller/Koordinator vor, der in der CLI einfach eine Klasse ist, die den Prozess steuert, und der sich an das Ereignis anmeldet.

import Foundation

class LibraryController {
    private let library: Library

    init(library: Library) {
        self.library = library
    }

    func start() {
        library.onBookAdded = {
            self.showAddedMessage()
        }
    }

    private func showAddedMessage() {
        print("Book added!") // Book added!
    }
}

Hier ist onBookAdded offensichtlich ein escaping-Kontext: Die Closure wird in der Eigenschaft onBookAdded gespeichert. Und die Methode start() erzeugt eine Closure, die self capturt. Deshalb wird self. obligatorisch: Das ist genau der Fall „warum man manchmal self. braucht“.

Man kann das auch mit [self] schreiben, wenn es für Sie lesbarer ist:

import Foundation

class LibraryController {
    private let library: Library

    init(library: Library) {
        self.library = library
    }

    func start() {
        library.onBookAdded = { [self] _ in
            showAddedMessage()
        }
    }

    private func showAddedMessage() {
        print("Book added!") // Book added!
    }
}

Achten Sie auf ein Detail: Wir haben _ in hinzugefügt, weil onBookAdded Book entgegennimmt, wir es aber nicht verwenden. Jetzt ist der Capture von self in der capture list klar sichtbar, und innerhalb des Blocks kann man ohne self. schreiben.

6. Sichere Praxis beim Capture von self

weak self und der „weak-strong dance“

Jetzt kommen wir zur Praxis, die ständig auftaucht: Die Closure wird irgendwo außerhalb gespeichert, aber wir wollen nicht, dass sie das Objekt festhält. Dafür verwendet man [weak self]. In diesem Fall wird self innerhalb der Closure zu einem Optional, weil das Objekt bereits zerstört sein könnte.

Das wirkt anfangs etwas beängstigend, aber die Logik ist sehr einfach: „Wenn das Objekt noch lebt, führen wir den Code aus; wenn nicht, verlassen wir die Closure stillschweigend.“

import Foundation

class LibraryController {
    private let library: Library

    init(library: Library) {
        self.library = library
    }

    func start() {
        library.onBookAdded = { [weak self] _ in
            guard let self else { return }
            self.showAddedMessage()
        }
    }

    private func showAddedMessage() {
        print("Book added!") // Book added!
    }
}

Hier sehen Sie den berühmten „weak-strong dance“: zuerst der Capture mit weak self, danach guard let self, der für die Laufzeit des Closure-Körpers eine starke Referenz erzeugt.

Diese Form guard let self else { return } ist keine „Magie“, sondern Sprachunterstützung für das Muster „weak self in strong self heben“ innerhalb einer Closure. Sie entstand als Antwort auf den typischen Schmerz mit strongSelf/ss/myVeryStrongSelf, die im Code überall auftauchten.

In modernen Swift-Versionen gibt es noch einen angenehmen Nebeneffekt: Nachdem Sie guard let self geschrieben haben, kann der Compiler in manchen Situationen erlauben, Methodenaufrufe ohne self. zu schreiben — weil Sie den Capture und den Kontrollfluss bereits explizit gemacht haben.

Aber wichtig ist: Selbst wenn der Compiler das Weglassen von self. nach dem Unwrapping erlaubt, ist Lesbarkeit wichtiger als Mode. Wenn das Team oder der Projektstil self. verlangt, verwenden Sie self.. Wenn es für Sie ohne leichter lesbar ist, verwenden Sie die Variante mit implicit self dort, wo sie erlaubt ist.

Eingebettete Closures: warum self wieder nötig sein kann

Im echten Code sind Closures oft verschachtelt: Sie speichern einen Callback, und innerhalb dieses Callbacks rufen Sie eine Funktion auf, die noch einen weiteren Callback akzeptiert. Und dann kann plötzlich wieder die Anforderung „explicit self“ auftauchen, selbst wenn die äußere Ebene self bereits mit [self] oder [weak self] capturt hat.

Der Grund ist sehr pragmatisch: Eine eingebettete Closure ist ein neues Closure-Objekt, mit eigener Lebensdauer und eigenem Satz an Captures. Dass die äußere Ebene „self schon gecapturt hat“, garantiert nicht, dass die innere Ebene keinen unauffälligen Retain-Zyklus erzeugt. Deshalb gilt die Regel „Capture explizit machen“ für jede escaping Closure einzeln.

Schauen wir uns ein Beispiel auf Ideenniveau an (ohne Asynchronität): Wir haben eine Funktion, die eine escaping Closure akzeptiert und sie irgendwo speichert.

import Foundation

func store(_ action: @escaping () -> Void) {
    // stellen wir uns vor, dass action irgendwo gespeichert wird
    action()
}

Nun die Klasse mit einer eingebetteten Closure:

import Foundation

class NestedDemo {
    var x: Int = 0

    func run() {
        store { [self] in
            store {
                self.x += 1
            }
        }
    }
}

Beachten Sie: Der inneren store { ... } muss weiterhin der Capture von self explizit gezeigt werden. Das ist ein normaler „Schutz vor Überraschungen“: Verschachtelung hebt die Regel nicht auf, dass jeder escaping-Kontext für sich verständlich sein muss.

Spickzettel: self., [self], [weak self]

Wenn Sie gerade erst anfangen, ist es leicht, in Extreme zu verfallen: entweder self. immer und überall zu schreiben, oder zu versuchen, self überall loszuwerden und mit dem Compiler zu kämpfen. Die Realität ist angenehmer: Es gibt mehrere „normale“ Varianten, und jede passt zu einem bestimmten Lebensdauervertrag.

Ansatz Wie es aussieht Was es speicherseitig bedeutet Wann es sinnvoll ist
Explizites self. innerhalb des Körpers
stored = { self.doWork() }
Starker Capture von self (wenn nicht weak/unowned) Wenn Sie den Capture maximal deutlich hervorheben möchten oder der Projektstil self. verlangt
Capture list [self]
stored = { [self] in doWork() }
Starker Capture von self, aber nur einmal deklariert Wenn Sie viele Zugriffe auf Mitglieder des Objekts haben und den visuellen Lärm reduzieren möchten, ohne unehrlich zu werden
Capture list [weak self] + guard let self
{ [weak self] in guard let self else { return }; ... }
Hält das Objekt nicht fest; der Code läuft nur, wenn das Objekt lebt Wenn die Closure „irgendwie lange“ lebt und das Objekt ohne Wartezeit deinitialisieren können soll

Die ganze Tabelle basiert auf einer Idee: Der Compiler möchte, dass Sie im escaping-Kontext die Capture-Politik explizit wählen, weil genau dort die unerwarteten Objektbindungen am häufigsten entstehen.

7. Typische Fehler bei der Arbeit mit self in escaping Closures

Fehler Nr. 1: self. mechanisch hinzufügen, „damit es halt kompiliert“.
So kann man den Compilerfehler zwar schnell beseitigen, aber man kann auch ebenso schnell einen retain cycle erzeugen, vor allem wenn die Closure in einer Eigenschaft des Objekts gespeichert wird. Die richtige Reaktion auf die Forderung nach self. ist nicht „den Punkt setzen“, sondern sich zu fragen: „Will ich, dass die Closure das Objekt festhält? Oder soll das Objekt ruhig verschwinden dürfen?“. Die Regel des expliziten self selbst entstand als Hinweis auf mögliche Retain-Zyklen.

Fehler Nr. 2: [self] als Methode verwenden, um „das self. zu entfernen und es zu vergessen“.
Die capture list [self] macht den Code hübscher, aber nicht automatisch sicherer. Es ist ein starker Capture, und wenn die Closure irgendwo gespeichert wird (besonders innerhalb von self), kann man ebenso leicht einen Retain-Zyklus bekommen wie mit explizitem self.. [self] steht für Lesbarkeit und die Klarheit der Absicht, nicht für „Anti-Leaks“.

Fehler Nr. 3: [weak self] schreiben und dann self! verwenden.
Das kommt häufiger vor, als man möchte, weil das Gehirn denkt: „Ich habe doch eine schwache Referenz gemacht — also ist alles sicher.“ Aber self! hebt die ganze Idee auf: Wenn das Objekt bereits zerstört wurde, bekommen Sie einen Crash. Verwenden Sie stattdessen self?.method() für einzelne Aufrufe oder guard let self else { return } für einen Logikblock.

Fehler Nr. 4: weak und unowned verwechseln und unowned „aus Kürzegründen“ wählen.
unowned ist ein Vertrag: „Das Objekt wird ganz sicher noch leben, wenn die Closure aufgerufen wird“. Wenn dieser Vertrag verletzt wird, gibt es einen Absturz. In Lehr- und Anwendungsszenarien, in denen die Closure lange leben kann und Sie den Aufrufzeitpunkt nicht kontrollieren, ist weak normalerweise sicherer. Code-Kürze ist ein schwacher Trost, wenn die App „einmal pro Woche bei einem Benutzer“ abstürzt.

Fehler Nr. 5: sich wundern, dass in verschachtelten Closures wieder explizites self verlangt wird.
Jede escaping Closure ist eine eigene Entität mit eigenem Capture. Dass die äußere Ebene self bereits capturt hat, bedeutet nicht, dass die innere Ebene kein neues Risiko erzeugt. Deshalb verlangt der Compiler auf jeder Ebene nach Klarheit — insbesondere damit in verschachtelten Konstruktionen keine unbemerkten Retain-Zyklen entstehen.

1
Aufgabe
Swift SELF, Level 38, Lektion 2
Gesperrt
Hallo später
Hallo später
1
Aufgabe
Swift SELF, Level 38, Lektion 2
Gesperrt
Präfix ohne Rauschen
Präfix ohne Rauschen
1
Aufgabe
Swift SELF, Level 38, Lektion 2
Gesperrt
Schwache Registrierung
Schwache Registrierung
1
Aufgabe
Swift SELF, Level 38, Lektion 2
Gesperrt
Zwei Schritte
Zwei Schritte
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION