Inout parameter in asynchronous callback not working properly

I am trying to insert functions with the inout to add data received from an asynchronous callback to an external array. However, this does not work. And I tried everything I know to find out why - without luck.

As recommended by @AirspeedVelocity, I rewrote the code as follows to remove unnecessary dependencies. I also use Int as an inout to make it simple.
The output is always:
c before: 0
c after: 1

I cannot understand what is wrong here.

 func getUsers() { let u = ["bane", "LiweiZ", "rdtsc", "ssivark", "sparkzilla", "Wogef"] var a = UserData() a.userIds = u a.dataProcessor() } struct UserData { var userIds = [String]() var counter = 0 mutating func dataProcessor() -> () { println("counter: \(counter)") for uId in userIds { getOneUserApiData(uriBase + "user/" + uId + ".json", &counter) } } } func getOneUserApiData(path: String, inout c: Int) { var req = NSURLRequest(URL: NSURL(string: path)!) var config = NSURLSessionConfiguration.ephemeralSessionConfiguration() var session = NSURLSession(configuration: config) var task = session.dataTaskWithRequest(req) { (data: NSData!, res: NSURLResponse!, err: NSError!) in println("c before: \(c)") c++ println("c after: \(c)") println("thread on: \(NSThread.currentThread())") } task.resume() } 

Thanks.

+7
swift
source share
4 answers

Let's say changing the inout in async-callback is pointless.

From an official document :

Parameters can provide default values ​​to simplify function calls, and can be passed as input parameters that modify the passed variable after it completes .

...

The in-out parameter has a value that is passed to the function, changed by the function, and passed back from the function to replace the original value.

Semantically, the in-out parameter is not "call-by-reference" , but "call by copy, restore . "

In your case, counter is only supported when returning getOneUserApiData() , and not in the dataTaskWithRequest() .

Here is what happened in your code

  • when getOneUserApiData() called, counter 0 copied to c 1
  • circuit locks c 1
  • call dataTaskWithRequest()
  • getOneUserApiData returned, and the value is unmodified - c 1 is written with feedback to counter
  • repetition procedure 1-4 for c 2 , c 3 , c 4 ...
  • ... extract from the Internet ...
  • callback is called and increments c 1 .
  • callback is called and increments c 2 .
  • callback is called and increments c 3 .
  • callback is called and increments c 4 .
  • ...

As a result, counter modified: (


Detailed explanation

Usually the in-out parameter is passed by reference, but this is simply the result of compiler optimization. When a closure captures the inout , "pass-by-reference" is not safe because the compiler cannot guarantee the lifetime of the original value. For example, consider the following code:

 func foo() -> () -> Void { var i = 0 return bar(&i) } func bar(inout x:Int) -> () -> Void { return { x++ return } } let closure = foo() closure() 

In this code, var i freed when foo() returned. If x is a reference to i , x++ causes an access violation. To prevent this race condition, Swift adopts a challenge-copy-restore strategy here.

+16
source share

Essentially, it looks like you are trying to capture the "inout-ness" of an input variable in a close, and you cannot do this - consider the following simpler case:

 // f takes an inout variable and returns a closure func f(inout i: Int) -> ()->Int { // this is the closure, which captures the inout var return { // in the closure, increment that var return ++i } } var x = 0 let c = f(&x) c() // these increment i c() x // but it not the same i 

At some point, the variable passed in ceases to be x and becomes a copy. This probably happens at the time of capture.

edit: @rintaros responds to nails - inout doesn't actually semantically follow a link

If you think about it, that makes sense. What if you did this:

 // declare the variable for the closure var c: ()->Int = { 99 } if 2+2==4 { // declare x inside this block var x = 0 c = f(&x) } // now call c() - but x is out of scope, would this crash? c() 

When locks are captured by variables, they must be created in memory so that they can remain alive even after the area they declared ends. But in the case of f above, he cannot do this - it is too late to declare x in such a way, x already exists. Therefore, I assume that it is copied as part of the closure creation. This is why incrementing a closing-closed version does not actually increase x .

+6
source share

I had a similar goal, and I faced the same problem when the results inside the closure were not tied to my global inout variables. @rintaro did a great job explaining why this was happening in the previous answer.

I am going to include here a generalized example of how I worked on this. In my case, I had several global arrays that I wanted to assign with closure, and then do something every time (without duplicating a bunch of code).

 // global arrays that we want to assign to asynchronously var array1 = [String]() var array2 = [String]() var array3 = [String]() // kick everything off loadAsyncContent() func loadAsyncContent() { // function to handle the query result strings // note that outputArray is an inout parameter that will be a reference to one of our global arrays func resultsCallbackHandler(results: [String], inout outputArray: [String]) { // assign the results to the specified array outputArray = results // trigger some action every time a query returns it strings reloadMyView() } // kick off each query by telling it which database table to query and // we're also giving each call a function to run along with a reference to which array the results should be assigned to queryTable("Table1") {(results: [String]) -> Void in resultsCallbackHandler(results, outputArray: &self.array1)} queryTable("Table2") {(results: [String]) -> Void in resultsCallbackHandler(results, outputArray: &self.array2)} queryTable("Table3") {(results: [String]) -> Void in resultsCallbackHandler(results, outputArray: &self.array3)} } func queryTable(tableName: String, callback: (foundStrings: [String]) -> Void) { let query = Query(tableName: tableName) query.findStringsInBackground({ (results: [String]) -> Void in callback(results: results) }) } // this will get called each time one of the global arrays have been updated with new results func reloadMyView() { // do something with array1, array2, array3 } 
0
source share

@rintaro perfectly explained why it doesn't work, but if you really want to, using UnsafeMutablePointer will do the trick:

 func getOneUserApiData(path: String, c: UnsafeMutablePointer<Int>) { var req = NSURLRequest(URL: NSURL(string: path)!) var config = NSURLSessionConfiguration.ephemeralSessionConfiguration() var session = NSURLSession(configuration: config) var task = session.dataTaskWithRequest(req) { (data: NSData!, res: NSURLResponse!, err: NSError!) in println("c before: \(c.memory)") c.memory++ println("c after: \(c.memory)") println("thread on: \(NSThread.currentThread())") } task.resume() } 
0
source share

All Articles