Achtergrond:
Closures als concept komen uit functionele talen en stellen ons in staat om codeblokken als waarden door te geven. In Swift zijn closures (vergelijkbaar met lambda's in andere talen) vanaf het begin aanwezig. Hierdoor kunnen we elegant asynchrone oproepen implementeren, acties delegeren, collecties sorteren, filteren, enzovoort.
Probleem:
Closures vangen variabelen uit de omliggende context. Als een van deze variabelen verwijzingen naar class-objecten bevat, kan er een retain cycle ontstaan, vooral als de closure als eigenschap van deze class wordt opgeslagen. Het is ook belangrijk om het verschil tussen escaping en non-escaping closures te begrijpen en te beseffen hoe waarde-overname werkt.
Oplossing:
Swift implementeert closures als zelfstandige objecten, ze kunnen de context vastleggen en de eigenaar van de closure moet voorzichtig zijn met de architectuur.
Voorbeeldcode:
class Performer { var onComplete: (() -> Void)? func doWork() { onComplete = { print("Werk voltooid!") } } } // Vastleggen van self binnen closure class Test { var value = 0 func start() { DispatchQueue.global().async { [weak self] in self?.value = 1 } } }
Belangrijke kenmerken:
Als de closure self niet expliciet vastlegt, kan er dan een retain cycle ontstaan?
Ja, als de closure wordt opgeslagen als een sterke eigenschap van de class en toegang heeft tot self binnenin, ontstaat er een retain cycle, zelfs als je self niet expliciet schrijft. Gebruik [weak self]/[unowned self] of maak de closure lokaal als dat mogelijk is.
Wat is het verschil tussen escaping en non-escaping closure?
Een escaping closure kan worden aangeroepen na de voltooiing van de functie en wordt meestal gebruikt voor asynchrone operaties. Non-escaping wordt uitgevoerd vóór het einde van de functie. Bij escaping is de volgorde van het vastleggen van self anders.
Voorbeeldcode:
func asyncWork(completion: @escaping () -> Void) { DispatchQueue.global().async { completion() } }
Kan een closure de waarden van de vastgelegde variabelen wijzigen?
Ja, als de closure een variabele heeft vastgelegd die als var is gedeclareerd, kan hij de waarde ervan wijzigen (voor waarde-typen). Voor een class zal dit een referentie zijn en kunnen eigenschappen altijd worden gewijzigd.
Voorbeeldcode:
var value = 1 let closure = { value += 1 } closure() print(value) // 2
ViewController slaat een closure op als een eigenschap (bijvoorbeeld completionHandler) en heeft binnenin directe toegang tot self. Dit leidt tot een retain cycle: ViewController => closure => ViewController. Het loskoppelen van het scherm bevrijdt het geheugen niet.
Voordelen: De code lijkt beknopt en kort.
Nadelen: Geheugenlekken, ViewController "blijft hangen" in het geheugen, potentiële bugs.
Gebruik [weak self] of [unowned self] binnen de closure, of zorg ervoor dat de closure niet langer leeft dan de levenscyclus van het object. Review dergelijke plekken tijdens code-review.
Voordelen: Correcte vrijgave van bronnen, geen onvoorspelbare lekken.
Nadelen: [weak self] vereist voorzichtigheid bij het dereferenceren, er kunnen onopzettelijke crashes optreden bij onjuist gebruik.