6

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

zoul
  • 102,279
  • 44
  • 260
  • 354

3 Answers3

10

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

Community
  • 1
  • 1
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Nice catch, thanks. Do you think it would make sense for the compiler to at least produce a warning when the specifier is missing in front of a reference type? – zoul Apr 04 '16 at 15:24
  • @zoul: No, see above example. – Martin R Apr 04 '16 at 15:35
  • Great, thanks! I think what makes me unhappy is that the simple, common case of `unowned x, y` looks misleading (cf. `int a, b` in C-like languages), while the rare case of no-specifier reference type capture could easily (and more explicitly!) be solved using a temporary variable. – zoul Apr 04 '16 at 15:37
1

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.

Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
  • Well, the question is why would one want to include an identifier in the capture list without modifying its capture specifier? But as Martin says, this actually makes sense for value types. Still, the behaviour with reference types is a bug waiting to happen, IMHO. – zoul Apr 04 '16 at 15:26
0

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.

jtbandes
  • 115,675
  • 35
  • 233
  • 266