Blog

Funktionsreferenzen in Swift und Beibehaltungszyklen

Lammert Westerhoff

Lammert Westerhoff

Aktualisiert Oktober 22, 2025
7 Minuten

Die Programmiersprache Swift verfügt über einige nette Funktionen. Eine dieser Funktionen sind Closures, die den Blöcken in Objective-C ähneln. Wie in den Apple-Anleitungen erwähnt, sind Funktionen spezielle Arten von Closures und auch sie können an andere Funktionen weitergegeben und als Eigenschaftswerte gesetzt werden. In diesem Beitrag werde ich einige Anwendungsbeispiele durchgehen und insbesondere die Gefahren von Retain-Zyklen erläutern, in die Sie schnell geraten können, wenn Sie Funktionszeiger beibehalten.

Werfen wir zunächst einen Blick auf ein recht einfaches Objective-C-Beispiel, bevor wir etwas Ähnliches in Swift schreiben.

Objective-c

Wir werden eine Schaltfläche erstellen, die eine Blockanweisung ausführt, wenn sie angetippt wird. In der Header-Datei definieren wir eine Eigenschaft für den Block: [objc] @interface[/objc]BlockButton : UIButton @property(nonatomic, strong) void (^action)(); @end Beachten Sie, dass dies eine starke Referenz ist und der Block und die Referenzen im Block erhalten bleiben. Und dann wird die Implementierung den Block ausführen, wenn er angetippt wird: [objc]

importieren "BlockButton.h"

@implementation BlockButton -(void)setAction:(void (^)())action { _action = action; [self addTarget:self action:@selector(performAction) forControlEvents:UIControlEventTouchUpInside]; } -(void)performAction { self.action(); } @end [/objc] Wir können diese Schaltfläche nun wie folgt in einem unserer Viewcontroller verwenden: [objc] self.button.action = ^{ NSLog(@"Button Tapped"); }; [/objc] Wir sehen jetzt jedes Mal, wenn wir auf die Schaltfläche tippen, die Meldung "Button Tapped" in der Konsole. Und da wir innerhalb unseres Blocks nicht auf self verweisen, bekommen wir keine Probleme mit Retain-Zyklen.In vielen Fällen ist es jedoch wahrscheinlich, dass Sie auf self verweisen, weil Sie vielleicht eine Funktion aufrufen wollen, die Sie auch an anderen Stellen aufrufen müssen. Lassen Sie uns ein solches Beispiel betrachten: [objc] -(void)viewDidLoad { self.button.action = ^{ [self buttonTapped]; [/objc] }; } -(void)buttonTapped { NSLog(@"Button Tapped"); } Da unser View-Controller (oder dessen View) unsere Schaltfläche behält und die Schaltfläche den Block behält, erzeugen wir hier einen Retain-Zyklus, da der Block eine starke Referenz auf self erzeugt. Das bedeutet, dass unser View-Controller niemals freigegeben wird und wir ein Speicherleck haben. Dies kann leicht gelöst werden, indem Sie eine schwache Referenz auf self verwenden: [objc] __weak typeof(self) weakSelf = self; self.button.action = ^{ [weakSelf buttonTapped]; }; [/objc] So weit nichts Neues, also machen wir weiter mit der Erstellung von etwas Ähnlichem in Swift.

Schnell

In Swift können wir einen ähnlichen Button erstellen, der statt eines Blocks eine Closure ausführt: [objc] class ClosureButton: UIButton { var action: (() -> ())? { didSet { addTarget(self, action: "callClosure", forControlEvents: .TouchUpInside) } } func callClosure() { if let action = action { action() [/objc] } } } Es tut dasselbe wie die Objective-C-Version (und in der Tat könnten Sie es aus Objective-C mit demselben Block wie zuvor verwenden). Wir können ihm von unserem View-Controller aus eine Aktion wie folgt zuweisen: [objc] button.action = { println("Button Tapped") } [/objc] Da diese Schließung self nicht erfasst, werden wir hier keine Probleme mit Retain-Zyklen haben. Wie bereits erwähnt, sind Funktionen nur eine spezielle Art von Schließungen. Das ist auch gut so, denn so können wir sofort auf Funktionen verweisen, wie hier: [objc] override func viewDidLoad() { button.action = buttonTapped } func buttonTapped() { println("Button Tapped") } [/objc] Schöne und einfache Syntax und gut für funktionale Programmierung. Wenn sie uns nur keine Probleme bereiten würde. Ohne dass es auf den ersten Blick ersichtlich ist, erzeugt das obige Beispiel einen Rückhaltezyklus. Und warum? Wir beziehen uns nirgendwo auf uns selbst? Oder doch? Das Problem ist, dass die Funktion buttonTapped Teil unserer View-Controller-Instanz ist. Wenn die button.action also auf diese Funktion verweist, erzeugt sie auch einen starken Verweis auf den View Controller. In diesem Fall könnten wir das Problem lösen, indem wir buttonTapped zu einer Klassenfunktion machen. Da Sie aber in den meisten Fällen in einer solchen Funktion etwas mit self machen wollen, z.B. auf Variablen zugreifen, ist dies keine Option. Das Einzige, was wir tun können, um dies zu beheben, ist sicherzustellen, dass die Schaltfläche keinen starken Verweis auf den View Controller erhält. Genau wie in unserem letzten Objective-C-Beispiel müssen wir einen schwachen Verweis auf self erstellen. Leider gibt es keine einfache Möglichkeit, einfach einen schwachen Verweis auf unsere Funktion zu erhalten. Also müssen wir uns hier etwas einfallen lassen.

Abhilfe 1: Einwickeln des Verschlusses

Wir können eine schwache Referenz erstellen, indem wir die Funktion in eine Schließung einschließen: [objc] button.action = { [weak self] in self!.buttonTapped() } [/objc] Hier erstellen wir zunächst eine schwache Referenz von self. Und in Swift sind schwache Referenzen immer optional. Das bedeutet, dass self in dieser Schließung jetzt optional ist und wir es zuerst auspacken müssen, wofür das Ausrufezeichen steht. Da wir wissen, dass dieser Code nicht aufgerufen werden kann, wenn self freigegeben wird, können wir das ! anstelle von ? verwenden. Das ist weit weniger elegant, als unsere Funktion sofort zu referenzieren. Theoretisch sollte die Verwendung einer nicht-eigenen Referenz auf self auch wie folgt funktionieren: [objc] button.action = { [unowned self] in self.buttonTapped() } [/objc] Leider stürzt dies (aus mir unbekannten Gründen) mit einem EXC_BAD_ACCESS bei der Deallokation des ClosureButton ab. Wahrscheinlich ein Fehler.

Abhilfe 2: Methode Zeigerfunktion

Dank einer Frage auf StackOverflow zu diesem Problem und einer Antwort von Rob Napier gibt es eine Möglichkeit, den Code wieder etwas eleganter zu gestalten. Wir können eine Funktion definieren, die das Wrapping in einer Closure für uns erledigt: [objc] func methodPointer<T: AnyObject>(obj: T, methode: (T) -)> () -> Void) -> (() -> Void) { return { [weak obj] in Methode(obj!)() } } [/objc] Jetzt können wir eine schwache Referenz auf unsere Funktion etwas einfacher erhalten. [objc] button.action = methodPointer(self, ViewController.buttonTapped) [/objc] Das funktioniert, weil Sie einen Verweis auf jede Instanzfunktion erhalten können, indem Sie sie als Klassenfunktion mit der Instanz (in diesem Fall self) als Argument aufrufen. Die folgenden Beispiele tun alle das Gleiche: [objc] // normaler Aufruf self.buttonTapped() // Referenz über Klasse erhalten let myFunction = MyViewController.buttonTapped(self) myFunction() // direkt über Klasse MyViewController.buttonTapped(self)() [/objc] Dies hat jedoch den Nachteil, dass es nur mit Funktionen funktioniert, die keine Argumente annehmen und Void zurückgeben, d.h. Methoden mit einer () -> () Signatur, wie unsere buttonTapped. Für jede Signatur müssten wir eine eigene Funktion erstellen. Zum Beispiel für eine Funktion, die einen String-Parameter annimmt und einen Int zurückgibt: [objc] func methodPointer<T: AnyObject>(obj: T, methode: (T) -)> (Zeichenfolge) -> Int) -> ((String) -> Int) { return { [weak obj] string in method(obj!)(string) } } [/objc] Wir können sie dann auf die gleiche Weise verwenden: [objc] func someFunction() { let myFunction = methodPointer(self, MyViewController.stringToInt) let myInt = myFunction("123") } func stringToInt(string: String) -> Int { return string.toInt() } [/objc]

Zyklen innerhalb einer einzelnen Klasseninstanz beibehalten

Retain-Zyklen treten nicht nur auf, wenn starke Referenzen zwischen zwei Instanzen einer Klasse hergestellt werden. Es ist auch möglich - und wahrscheinlich weniger offensichtlich - eine starke Referenz innerhalb derselben Instanz zu erzeugen. Sehen wir uns ein Beispiel an: [objc] var print: ((String) -> ())? override func viewDidLoad() { print = printToConsole } func printToConsole(message: String) { println(message) } [/objc] Hier machen wir so ziemlich dasselbe wie in unseren Schaltflächenbeispielen. Wir definieren eine optionale Closure-Variable und weisen ihr dann eine Funktionsreferenz zu. Dadurch wird eine starke Referenz von der Druckvariablen zu self erzeugt und somit ein Retain-Zyklus geschaffen. Wir müssen das Problem mit den gleichen Tricks lösen, die wir schon früher angewandt haben. Ein weiteres Beispiel ist die Definition einer lazy-Variable. Da faule Variablen nach der Initialisierung zugewiesen werden, dürfen sie direkt auf self verweisen. Das heißt, wir können sie wie folgt auf eine Funktionsreferenz setzen: [objc] lazy var print: ((String) -> ()) = self.printToConsole [/objc] Das erzeugt natürlich auch einen Retain-Zyklus.

Fazit

Um Retain-Zyklen in Swift zu vermeiden, sollten Sie immer daran denken, dass eine Referenz auf eine Instanzfunktion bedeutet, dass Sie auch auf die Instanz referenzieren. Wenn Sie also einer Variablen zuweisen, erzeugen Sie eine starke Referenz. Achten Sie immer darauf, solche Referenzen in eine Closure mit einer schwachen Referenz auf die Instanz zu verpacken oder stellen Sie sicher, dass Sie die Variablen manuell auf Null setzen, wenn Sie mit ihnen fertig sind. Leider unterstützt Swift keine schwachen Closure-Variablen, was das Problem lösen würde. Hoffentlich wird es in Zukunft unterstützt oder es wird eine Möglichkeit geben, eine schwache Referenz auf eine Funktion zu erstellen, ähnlich wie wir jetzt [weak self] in Closures verwenden können.

Verfasst von

Lammert Westerhoff

Contact

Let’s discuss how we can support your journey.