异步回调中的Inout参数无法按预期工作

时间:2015-01-31 00:49:11

标签: swift

我尝试插入带有inout参数的函数,以将从异步回调接收的数据附加到外部数组。但是,它不起作用。我尝试了所知的一切,找出原因 - 没有运气。

正如@AirspeedVelocity所建议的,我重写了以下代码以删除不必要的依赖项。我还使用Int作为inout参数来保持简单。
输出始终是:
c before: 0
c after: 1

我无法弄清楚这里出了什么问题。

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()
}

感谢。

4 个答案:

答案 0 :(得分:16)

不幸的是,修改inout在异步回调参数是无意义的。

来自the official document

  

参数可以提供默认值来简化函数调用,并且可以作为输入输出参数传递,这些参数在函数完成执行后修改传递的变量

     

...

     

输入输出参数具有传递给函数的值,由函数修改,并且传回退出函数以替换原始值。

从语义上讲,输入输出参数不是"call-by-reference",而是"call-by-copy-restore"

在您的情况下,counter仅在getOneUserApiData()返回时被写入支持,而不是dataTaskWithRequest()回调。

以下是您的代码中发生的事情

  1. getOneUserApiData()来电时,counter 0的值已复制到c 1
  2. 闭包捕获c 1
  3. 致电dataTaskWithRequest()
  4. getOneUserApiData返回,并且 - 未修改 - c 1 的值被写入后备counter
  5. 重复1-4 c 2 c 3 c 4 的程序...
  6. ......从网上取货......
  7. 调用回调,c 1 递增。
  8. 调用回调,c 2 递增。
  9. 调用回调,c 3 递增。
  10. 调用回调,c 4 递增。
  11. ...
  12. 因此counter未经修改:(


    详细解释

    通常,in-out参数通过引用传递,但它只是编译器优化的结果。当闭包捕获inout参数时,“pass-by-reference”是不安全,因为编译器无法保证原始值的生命周期。例如,请考虑以下代码:

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

    在此代码中,var i返回时释放foo()。如果xi的引用,则x++会导致访问冲突。为了防止这种竞争条件,Swift采用了“逐个复制恢复”策略。

答案 1 :(得分:6)

基本上看起来你试图在闭包中捕获输入变量的“inout-ness”,而你不能这样做 - 考虑以下更简单的情况:

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

在某些时候,传入的变量不再是x而是变成副本。这可能发生在捕获点。

编辑:@ rintaro的答案指出 - inout实际上并没有通过引用进行语义传递

如果你仔细想想,这是有道理的。如果你这样做会怎么样:

// 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()

当闭包捕获变量时,它们需要在内存中创建,即使在声明结束的范围之后它们也可以保持活动状态。但是对于f以上的情况,它无法做到这一点 - 以这种方式声明x为时已晚,x已经存在。所以我猜它会被复制作为闭包创建的一部分。这就是为什么递增闭包捕获版本实际上不会增加​​x

答案 2 :(得分:0)

我有一个类似的目标并遇到了同样的问题,其中闭包内的结果没有分配给我的全局输入变量。 @rintaro很好地解释了为什么在之前的答案中出现这种情况。

我将在这里包含一个关于我如何解决这个问题的概括示例。在我的例子中,我想要在闭包中分配几个全局数组,然后每次都做一些事情(不重复一堆代码)。

$(".frequency_new_selector").on("change", function() {
  if (this.value === '1') {
    event.preventDefault();
    $('.cd-popup').addClass('is-visible');
  }
});

答案 3 :(得分:0)

@rintaro完美地解释了为什么它不起作用,但是如果你真的想这样做,使用UnsafeMutablePointer就可以解决问题:

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()
}