Inout parameter in async callback does not work as expected

前端 未结 4 1673
有刺的猬
有刺的猬 2020-11-29 12:26

I\'m trying to insert functions with inout parameter to append data received from async callback to an outside array. However, it does not work. And I tried eve

相关标签:
4条回答
  • 2020-11-29 12:58

    @rintaro perfectly explained why it doesn't work, but if you really want to do that, 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 讨论(0)
  • 2020-11-29 13:05

    Essentially it looks like you’re trying to capture the “inout-ness” of an input variable in a closure, and you can’t do that – 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's not the same i
    

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

    edit: @rintaro’s answer nails it – inout is not in fact semantically pass by reference

    If you think about it this 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 closures capture variables, they need to be created in memory in such a way that they can stay alive even after the scope they were declared ends. But in the case of f above, it can’t do this – it’s too late to declare x in this way, x already exists. So I’m guessing it gets copied as part of the closure creation. That’s why incrementing the closure-captured version doesn’t actually increment x.

    0 讨论(0)
  • 2020-11-29 13:09

    Sad to say, modifying inout parameter in async-callback is meaningless.

    From the official document:

    Parameters can provide default values to simplify function calls and can be passed as in-out parameters, which modify a passed variable once the function has completed its execution.

    ...

    An in-out parameter has a value that is passed in to the function, is modified by the function, and is passed back out of the function to replace the original value.

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

    In your case, counter is write-backed only when getOneUserApiData() returns, not in dataTaskWithRequest() callback.

    Here is what happened in your code

    1. at getOneUserApiData() call, the value of counter 0 copied to c1
    2. the closure captures c1
    3. call dataTaskWithRequest()
    4. getOneUserApiData returns, and the value of - unmodified - c1 is write-backed to counter
    5. repeat 1-4 procedure for c2, c3, c4 ...
    6. ... fetching from the Internet ...
    7. callback is called and c1 is incremented.
    8. callback is called and c2 is incremented.
    9. callback is called and c3 is incremented.
    10. callback is called and c4 is incremented.
    11. ...

    As a result counter is unmodified :(


    Detailed explaination

    Normally, in-out parameter is passed by reference, but it's just a result of compiler optimization. When closure captures inout parameter, "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 is freed when foo() returns. If x is a reference to i, x++ causes access violation. To prevent such race condition, Swift adopts "call-by-copy-restore" strategy here.

    0 讨论(0)
  • 2020-11-29 13:12

    I had a similar goal and ran into the same issue where results inside the closure were not being assigned to my global inout variables. @rintaro did a great job of explaining why this is the case in a previous answer.

    I am going to include here a generalized example of how I worked around this. In my case I had several global arrays that I wanted to assign to within a closure, and then do something each 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's 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 讨论(0)
提交回复
热议问题