zoul
zoul

Reputation: 104065

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]?

Upvotes: 6

Views: 2071

Answers (3)

Martin R
Martin R

Reputation: 539815

... 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.

Upvotes: 10

Sergey Kalinichenko
Sergey Kalinichenko

Reputation: 726659

According to the syntax EBNF, this treatment of unowned capture specifier is fully intentional:

closure-signature → parameter-clause­ function-result­opt in­
closure-signature → identifier-list­ function-result­opt ­in­
closure-signature → capture-list­ parameter-clause ­function-resultopt in­
closure-signature → capture-list ­identifier-list ­function-result­opt ­in
­ closure-signature → capture-list­ in­
capture-list → [­capture-list-items­]­
capture-list-items → capture-list-item­ capture-list-item ­capture-list-items
­ capture-list-item → capture-specifieropt ­expression­
capture-specifier → weak­ | unowned­ | unowned(safe)­ | unowned(unsafe)­

The three lines at the bottom defining <capture-list-items>, <capture-list-item>, and <capture-specifier> productions are most relevant here.

The <capture-list-items> production is a comma-separated list of <capture-list-item>, with the capture-specifier is attached to each individual <capture-list-item>, not to <capture-list-items> list as a whole.

This makes perfect sense, because it gives programmers full control over capturing of individual arguments. The alternative when the specifier would apply to the whole list would take away this flexibility.

why would one want to include an identifier in the capture list without modifying its capture specifier?

It appears that the philosophy of Swift's designers is to provide smart default behaviors whenever it is possible. In most cases, Swift can figure out a way to capture an expression that makes most sense based on the type of the expression without any involvement from the programmer. Explicit capture specifier is left for exceptional situations, when the compiler does not have enough information to figure out the proper way of capturing a variable based on the context.

Upvotes: 1

jtbandes
jtbandes

Reputation: 118691

To answer your specific questions:

Why is the capture specifier optional in capture lists?

Because the default behavior is to capture any necessary variables (reference types are captured strongly). By default, you don't need to specify them explicitly in the capture list if you want to use their values. (Although qualifying with self.property will be necessary if you are capturing self.)

…is there a reason [unowned x, y] is not treated as [unowned x, unowned y]?

For the same reason: the default is to capture strongly. The unowned doesn't apply to other items in the capture list; that's just not how the syntax works right now.

Upvotes: 0

Related Questions