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 explizitesself
im Closurespeak = { [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