Self in Closures - wurzelsand/swift-memos GitHub Wiki

Self in Closures

Themen

  • Retain Cycle

  • weak, unowned, Capture List

  • deinit

Aufgabe

Wir erzeugen innerhalb des do-Blocks ein Objekt der Klasse Person. Eigentlich sollte das Objekt nach Beendigung des do-Blocks deinitialisiert werden. Da wir hier aber ein Retain-Cycle haben, wird deinit nicht aufgerufen:

class Person {
    var sentence: String = "Hello!"
    var speak: () -> Void = {}
    
    init() {
        speak = {
            print(self.sentence)
        }
    }
    
    deinit {
        print("deinit")
    }
}

do {
    let peter = Person()
    peter.sentence = "Bye!"
    peter.speak()
}

Welche Möglichkeiten gibt es, den Retain-Cycle aufzulösen?

Ausführung

  • unowned

    speak = { [unowned self] in
        print(self.sentence) // Bye!
    }
    
  • unowned; ohne explizites self im Closure

    speak = { [unowned self] in
        print(sentence) // Bye!
    }
    
  • capture

    speak = { [sentence] in
        print(sentence) // Hello!
    }
    
  • weak

    speak = { [weak self] in
        if let self = self {
            print(self.sentence) // Bye!
        }
    }
    

Anmerkungen

self in einem Closure erzeugt nur dann ein Retain Cycle, wenn das Objekt eine Referenz auf das Closure speichert. Im folgenden gibt es daher kein Retain Cycle:

import Foundation

class Person {
    var sentence: String = "Hello!"
    
    func speak() {
        DispatchQueue.global(qos: .default).async {
            print(self.sentence)
            dispatchGroup.leave()
        }
    }
    
    deinit {
        print("deinit")
    }
}

let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
do {
    let peter = Person()
    peter.sentence = "Bye!"
    peter.speak()
}
dispatchGroup.wait()

Sehr wohl ein Retain Cycle gibt es aber in dieser Closure-Variante:

class Person {
    var sentence: String = "Hello!"
    
    lazy var speak: () -> Void = {
        DispatchQueue.global(qos: .default).async {
            print(self.sentence)
            dispatchGroup.leave()
        }
    }
    
    deinit {
        print("deinit")
    }
}

Offensichtlich erhält das speak-Closure eine Strong Reference von peter, sobald es zum ersten Mal aufgerufen wird. Gleichzeitig besitzt peter eine Strong Reference des Closures speak. Wenn speak hingegen eine Funktion ist, besitzt peter keine Referenz von speak, weil speak vermutlich nicht wirklich Teil des Person-Objekts peter ist, wie ich aus dem alternativen Methodenaufruf erkenne:

Person.speak(peter)()

speak scheint also in Wirklichkeit eine globale Funktion im Person-Namensbereich zu sein, der man das Objekt, auf den es sich beziehen soll, als Parameter mitgibt:

func speak(_ person: Person) {
    DispatchQueue.global(qos: .default).async {
        print(person.sentence)
        dispatchGroup.leave()
    }
}

extension Person {
    static func speak(_ person: Person) -> () -> Void {
        return { My_Project.speak(person) }
    }
}

Retain Cycle versteckt 1

Manchmal erkennt man nicht auf den ersten Blick, dass ein Objekt eine Referenz auf ein Closure speichert. Bei SwiftBySundell bin ich z. B. auf so etwas gestoßen:

class CanvasViewController {
    private var canvas = Canvas()
    private lazy var previewImageView = ImageView()
    
    func renderPreviewImage() {
        canvas.renderAsImage { [weak self] image in
            self?.previewImageView.image = image
        }
    }
}

Wieso sollte hier weak notwendig sein? Schließlich speichert canvas nicht das Closure sondern übergibt es nur einem seiner Methoden als Parameter. Und tatsächlich ergibt sich nicht zwangsweise ein Retain-Cycle, deinit würde also auch ohne weak aufgerufen werden:

class Image {}

class ImageView {
    var image = Image()
}

class Canvas {
    private var image = Image()
    
    func renderAsImage(render: @escaping (Image) -> Void) { // escaping not necessary here
        render(image)
    }
}

class CanvasViewController {
    private var canvas = Canvas()
    private lazy var previewImageView = ImageView()
    
    func renderPreviewImage() {
        canvas.renderAsImage { image in
            self.previewImageView.image = image
        }
    }
    
    deinit {
        print("deinit")
    }
}

do {
    let canvasViewController = CanvasViewController()
    canvasViewController.renderPreviewImage()
}

Anders sieht es aber aus, wenn der Closure-Parameter in der Methode ein Escaping-Closure ist und in Canvas gespeichert wird:

class Image {}

class ImageView {
    var image = Image()
}

class Canvas {
    private var image = Image()
    var renderFunction: (Image) -> Void = { _ in }
    
    func renderAsImage(render: @escaping (Image) -> Void) {
        renderFunction = render
        render(image)
    }
}

class CanvasViewController {
    private var canvas = Canvas()
    private lazy var previewImageView = ImageView()
    
    func renderPreviewImage() {
        canvas.renderAsImage { [weak self] image in
            self?.previewImageView.image = image
        }
    }
    
    deinit {
        print("deinit")
    }
}

do {
    let canvasViewController = CanvasViewController()
    canvasViewController.renderPreviewImage()
}

Wenn wir also eine Methode einer Instanzvariablen unserer Klasse aufrufen und ihr ein Closure, das eine Referenz auf self enthält, als Parameter übergeben, muss dieses self entweder weak oder unowned sein.

Retain Cycle versteckt 2

Auch hier kann es sein, dass man den Retain Cycle übersieht:

class Cupboard {
    var drawer = Drawer()
    let color: String
    
    init(color: String) {
        self.color = color
        print("+ Cupboard")
    }
    
    deinit {
        print("- Cupboard")
    }
}

class Drawer {
    var logger: () -> Void = {}
    
    init() {
        print("+ Drawer")
    }
    
    deinit {
        print("- Drawer")
    }
}

func makeDrawer(for cupboard: Cupboard) -> Drawer {
    let drawer = Drawer()
    drawer.logger = {
        print("I am a drawer of a cupboard in", cupboard.color)
    }
    return drawer
}

do {
    let cupboard = Cupboard(color: "brown")
    cupboard.drawer = makeDrawer(for: cupboard)
    cupboard.drawer.logger()
}

Ausgabe:

+ Drawer
+ Cupboard
+ Drawer
- Drawer
I am a drawer of a cupboard in brown

Sowohl ein Drawer als auch ein Cupboard werden nicht wieder frei gegeben.

Daher:

...
drawer.logger = { [unowned cupboard] in
    print("I am a drawer of a cupboard in", cupboard.color)
}

Ausgabe:

+ Drawer
+ Cupboard
+ Drawer
- Drawer
I am a drawer of a cupboard in brown
- Cupboard
- Drawer