Do we need to explicitly use capture list for weak variables in swift closure?

1.5k views Asked by At

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.

1

There are 1 answers

5
Rob Napier On BEST ANSWER

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: %15, %8, %7, %2, %22, %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, %15, %2, %22, %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.