Search code examples
swiftretain-cycle

Why is the capture specifier optional in capture lists?


There seems to be a curious syntax glitch in the capture list syntax in Swift. If I declare multiple captured variables, the capture specifier only applies to the first one:

let closure = { [unowned x, y] in … }

Now I would expect y to be unowned, but it doesn’t seem to be the case:

class Test {

    var callback: (Void -> Void)?

    init() {
        print("init")
    }

    deinit {
        print("deinit")
    }
}

func makeScope() {
    let x = Test()
    let y = Test()
    y.callback = { [unowned x, y] in
        print(y)
    }
}

makeScope()
print("done")

This prints:

init
init
deinit
done

So y seems to be captured strongly and creates a retain cycle, preventing the object from being deallocated. Is that so? If yes, does it make sense to permit an “empty” capture specifier in the list? Or is there a reason [unowned x, y] is not treated as [unowned x, unowned y]?


Solution

  • ... does it make sense to permit an “empty” capture specifier in the list?

    Yes it does. The capture specifiers ("weak", "unowned" and its variations) can only be used with reference types, but there are also cases where you want to capture a value type (here is one example: Pass value to closure?).

    You also may want to capture a reference type strongly. Capturing a reference type ensures that the reference (pointer) itself is captured by value, as demonstrated in the following example:

    class MyClass {
        let value : String
        init(value : String) {
            self.value = value
        }
    }
    
    var ref = MyClass(value: "A")
    
    let clo1: () -> Void = { print(ref.value) }
    let clo2: () -> Void = { [ref] in print(ref.value) }
    
    ref = MyClass(value: "B")
    
    clo1() // Output: B
    clo2() // Output: A
    

    When the first closure is executed, ref inside the closure is a reference to the object created as MyClass(value: "B").

    The second closure captures the value of ref at the time the closure is created, and this does not change when a new value is assigned to var ref.