1

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.

Vishal Singh
  • 4,400
  • 4
  • 27
  • 43
  • 1
    Yes, they behave the same with respect to memory management. So, there’s no absolutely no reason to use the `weak var weakSelf = self` pattern. It only makes one look like an Objective-C developer who is unfamiliar with common Swift practices. – Rob Mar 27 '22 at 18:19
  • BTW, [SE-0079](https://github.com/apple/swift-evolution/blob/main/proposals/0079-upgrade-self-from-weak-to-strong.md) eliminates the other half of the Objective-C `weakSelf`/`strongSelf` dance. – Rob Mar 27 '22 at 18:22
  • Got it. In terms of memory management, they behave the same, but they are different in terms of their scope. For example, capture list creates it own variable, so any new assignments (or any mutation for value types ) outside the closure wont be tracked, while without capture list will track any new assignments (or any mutation for value types) outside the closure as it is basically the same variable. – Vishal Singh Mar 27 '22 at 18:55

1 Answers1

4

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.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • 1
    Makes sense. Another difference (although not sure if its obvious) I noted in capture list vs the other variant, is the when we capture it via capture list, the scope of the captured variable is within the the scope of closure, while in other case, the scope is at module level. Ref: https://stackoverflow.com/questions/68695701/how-closure-captures-values-in-swift – Vishal Singh Mar 27 '22 at 15:46
  • 1
    For example, if I do, weak var weakObj = someStrongObj; closure = { weakObj?.run() }; weakObj = nil; closure(), then weakObj will be nil when the closure is run. While if we capture it in capture list, then closure = { [weak someStrongObj] in someStrongObj?.run() }; someStrongObj = nil; closure(), then the obj in closure may or may not be nil depending on if there exists any strong reference to the strongObj elsewhere – Vishal Singh Mar 27 '22 at 15:46
  • Slightly related, is it correct to say that capture list increases the retain count by 1 if captured strongly (`closure = { [obj] in obj.run() }`), while directly using the variable (`closure = { obj.run() }`) wont increase the retain count but will increase the scope of the variable? `isKnownUniquelyReferenced(&obj)` returns false for capture list but true for directly using the variable without capture list. – Vishal Singh Mar 27 '22 at 19:07
  • I would not rely on that. At a minimum it's going to depend on whether `closure` escapes. In some cases Swift can inline non-escaping closures, and also can sometimes remove redundant retains, but whether it does so or not is an implementation detail of the optimizer. (I would, for example, make sure you're testing this with and without `-O`.) It's very tricky to reason about these kinds of things in today's Swift. To understand how Swift will improve on this, see https://github.com/apple/swift/blob/main/docs/OwnershipManifesto.md – Rob Napier Mar 27 '22 at 20:20
  • 1
    All great questions, by the way, but I expect you'll find many of them are going to have answers like "mostly, but it's not promised." From a correctness point of view, just always make sure you have a strong reference to anything you care about, and drop that reference whenever you don't care about it anymore, and let the system do it's job. From a performance point of view, though, you often have to dig in and see what the compiler outputs. You might find interesting: https://twitter.com/jckarter/status/1508108978793828353 – Rob Napier Mar 27 '22 at 20:26