Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift closure capture and inout variables

Tags:

swift

Consider the following code:

func foo(inout success: Bool) -> (()->()) {
    return { _ in
        success = true
        print (success)
    }
}

var success = false
let closure = foo(&success)

closure()          //prints "true"
print(success)     //prints "false"

The closure appears to be creating a copy of success and does not change the original. Why is this taking place? I had assumed that the closure would point to the original because we are passing an inout variable.

like image 301
rob Avatar asked Apr 25 '16 21:04

rob


People also ask

Does closure capture values in Swift?

A closure can capture constants and variables from the surrounding context in which it's defined. The closure can then refer to and modify the values of those constants and variables from within its body, even if the original scope that defined the constants and variables no longer exists.

How do closures capture references to variables by default?

In Swift, closures capture the variables they reference: variables declared outside of the closure but that you use inside the closure are retained by the closure by default, to ensure they are still alive when the closure is executed.

Can a closure be assigned to a variable Swift?

How A Closure Works In Swift. Closures are self-contained blocks of functionality that can be passed around and used in your code. Said differently, a closure is a block of code that you can assign to a variable. You can then pass it around in your code, for instance to another function.

Are closures value or reference types?

The Basics. In Swift, structs, enums and tuples are all value types, while classes and closures are reference types. In a nutshell, a value type contains data and a reference type contains the location in memory where data lives.


2 Answers

It makes sense that this wouldn't update your success variable because your inout parameter is a parameter of foo, not of the closure itself. You get the desired behavior if you make the inout parameter a parameter of the closure:

var success = false
let closure = { (inout flag: Bool) -> () in
    flag = true
    print(flag)
}

closure(&success)  //prints "true"
print(success)     //prints "true"

This pattern also works with the function, too, as long as you keep the inout parameter a parameter of the closure:

func foo() -> ((inout Bool)->()) {
    return { flag in
        flag = true
        print (flag)
    }
}

var success = false
let closure = foo()

closure(&success)  //prints "true"
print(success)     //prints "true"

You also get the desired behavior if you use a reference type:

class BooleanClass: CustomStringConvertible {
    var value: Bool

    init(value: Bool) {
        self.value = value
    }

    var description: String { return "\(value)" }
}

func foo(flag: BooleanClass) -> (()->()) {
    return {
        flag.value = true
        print (flag)
    }
}

let success = BooleanClass(value: false)
let closure = foo(success)

closure()          //prints "true"
print(success)     //prints "true"
like image 185
Rob Avatar answered Sep 21 '22 07:09

Rob


This seems to be covered by Swift Evolution proposal 0035, and is considered a bug.

The document there refers to the inout parameter to the function as "a shadow copy that is written back to the argument when the callee returns". This seems to mean that there is, in essence, a temporary variable named success in the executing context of foo(). The value of that temp is then put into the outer success only when foo() returns.

Since in your foo(), the closure has not run when foo() returns, the value of the "shadow copy" has not changed. The outer success keeps its value.

The important part is that the closure has captured the shadow copy, not the outer success as you expect. So when the closure finally runs, that variable's value does change, but it no longer has any connection to the original outer success.

The proposal uses this snippet to demonstrate:

func captureAndEscape(inout x: Int) -> () -> Void {
  let closure = { x += 1 }
  closure()
  return closure
}

var x = 22
let closure = captureAndEscape(&x)
print(x) // => 23
closure()
print("still \(x)") // => still 23
like image 30
jscs Avatar answered Sep 17 '22 07:09

jscs