My understanding for closure was, it will capture all directly referenced objects strongly irrespective of whether the object variable was declared weak
or strong
outside the closure and, if we want to capture them weakly, then we need to explicitly define a capture list and mark them weak
in that capture list.
obj?.callback = { [weak obj] in
obj?.perform()
}
However in my test, I found that if the variable is already weak
outside the closure, then we don't need to use capture list to capture it weakly.
class Foo {
var callback: (() -> ())?
init() {
weak var weakSelf = self
callback = {
weakSelf?.perform()
}
// is above equivalent to below in terms of memory management?
// callback = { [weak self] in
// self?.perform()
// }
}
func perform() {
print("perform")
}
deinit {
print("deinit")
}
}
let foo = Foo() // prints "deinit foo"
The above snippet is not creating any retain cycle. Does this mean we don't need to explicitly capture an object variable weakly in capture list if the variable is already declared weak
and capture list just provides syntactical advantage over creating a weak
variable before using them inside closure.
CodePudding user response:
Sort of, in this specific example, but you need to be very careful about how you think about what's happening.
First, yes, this is identical. We can tell that by generating the SIL (swiftc -emit-sil main.swift
). Except for the difference in the name of self
vs weakSelf
, these generate exactly the same unoptimized SIL. In order to make it even clearer, I'll change the name of the variable in the capture list (this is just a renaming, it doesn't change the behavior):
weakSelf
weak var weakSelf = self
callback = {
weakSelf?.perform()
}
weak_self
callback = { [weak weakSelf = self] in
weakSelf?.perform()
}
Compare them
$ swiftc -emit-sil weakSelf.swift > weakSelf.sil
$ swiftc -emit-sil weak_self.swift > weak_self.sil
$ diff -c weakSelf.sil weak_self.sil
*** weakSelf.sil 2022-03-27 10:58:13.000000000 -0400
--- weak_self.sil 2022-03-27 11:01:22.000000000 -0400
***************
*** 102,108 ****
// Foo.init()
sil hidden @$s4main3FooCACycfc : $@convention(method) (@owned Foo) -> @owned Foo {
! // %0 "self" // users: , %8, %7, %2, ", %1
bb0(%0 : $Foo):
debug_value %0 : $Foo, let, name "self", argno 1, implicit // id: %1
%2 = ref_element_addr %0 : $Foo, #Foo.callback // user: %4
--- 102,108 ----
// Foo.init()
sil hidden @$s4main3FooCACycfc : $@convention(method) (@owned Foo) -> @owned Foo {
! // %0 "self" // users: %8, %7, , %2, ", %1
bb0(%0 : $Foo):
debug_value %0 : $Foo, let, name "self", argno 1, implicit // id: %1
%2 = ref_element_addr %0 : $Foo, #Foo.callback // user: %4
Except for the order of the users comment for self
, they're identical.
But be very, very careful with what you do with this knowledge. This happens to be true because there exists a strong reference to self
elsewhere. Going down this road too far can lead to confusion. For example, consider these two approaches:
// Version 1
weak var weakObject = Object()
let callback = {
weakObject?.run()
}
callback()
// Version 2
var weakObject = Object()
let callback = { [weak weakObject] in
weakObject?.run()
}
callback()
These do not behave the same at all. In the first version, weakObject
is already released by the time callback
is created, so callback()
does nothing. The compiler will generate a warning about this, so in most cases this is an unlikely bug to create, but as a rule you should generally do weak captures in the capture list so that it occurs as close to the closure creation as possible, and won't accidentally be released unexpectedly.