El parámetro Inout en la callback asíncrona no funciona como se esperaba

Estoy tratando de insertar funciones con inout parámetro inout para anexar los datos recibidos de la callback asíncrona a una matriz externa. Sin embargo, no funciona. Y probé todo lo que sé para descubrir por qué, sin suerte.

Como aconsejó @AirspeedVelocity, reescribí el código de la siguiente manera para eliminar dependencias innecesarias. También uso un parámetro Int como inout para mantenerlo simple.
La salida es siempre:
c before: 0
c after: 1

No soy capaz de descubrir qué va mal aquí.

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

Gracias.

Es triste decirlo, modificar el parámetro inout en async-callback no tiene sentido.

Del documento oficial :

Los parámetros pueden proporcionar valores predeterminados para simplificar las llamadas a funciones y se pueden pasar como parámetros de entrada y salida, que modifican una variable pasada una vez que la función ha completado su ejecución .

Un parámetro de entrada / salida tiene un valor que se transfiere a la función, es modificado por la función y se revierte de la función para reemplazar el valor original.

Semánticamente, el parámetro de entrada / salida no es “llamada por referencia” , sino “recuperación de llamada por copia” .

En su caso, el counter está respaldado por escritura solo cuando getOneUserApiData() retorna, no en dataTaskWithRequest() callback.

Esto es lo que sucedió en tu código

  1. en la llamada getOneUserApiData() , el valor del counter 0 copió en c 1
  2. el cierre captura c 1
  3. llamar a dataTaskWithRequest()
  4. getOneUserApiData regresa, y el valor de – no modificado – c 1 está respaldado por escritura en el counter
  5. repita el procedimiento 1-4 para c 2 , c 3 , c 4
  6. … obteniendo de Internet …
  7. se llama a la callback y c 1 se incrementa.
  8. se llama a la callback y c 2 se incrementa.
  9. se llama a la callback y c 3 se incrementa.
  10. se llama a la callback y c 4 se incrementa.

Como resultado, el counter no se modifica 🙁


Explicación detallada

Normalmente, el parámetro in-out se pasa por referencia, pero es solo el resultado de la optimización del comstackdor. Cuando el cierre captura el parámetro inout , “pass-by-reference” no es seguro , porque el comstackdor no puede garantizar la duración del valor original. Por ejemplo, considere el siguiente código:

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

En este código, var i se libera cuando devuelve foo() . Si x es una referencia a i , x++ causa una infracción de acceso. Para evitar dicha condición de carrera, Swift adopta la estrategia de “recuperar llamada por copia” aquí.

Esencialmente parece que estás tratando de capturar la “inout-ness” de una variable de entrada en un cierre, y no puedes hacer eso, considera el siguiente caso más simple:

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

En algún punto, la variable pasada deja de ser x y se convierte en una copia. Esto probablemente esté sucediendo en el punto de captura.

editar: la respuesta de @rintaro lo clava – en realidad no se pasa semánticamente por referencia

Si lo piensas, esto tiene sentido. ¿Y si hicieras esto?

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

Cuando los cierres capturan variables, deben crearse en la memoria de tal forma que puedan mantenerse con vida incluso después de que el scope en el que se declararon termina. Pero en el caso de f , no puede hacer esto; es demasiado tarde para declarar x de esta manera, x ya existe. Así que supongo que se copiará como parte de la creación de cierre. Es por eso que incrementar la versión capturada por cierre en realidad no incrementa x .

Tenía un objective similar y me encontré con el mismo problema en el que los resultados dentro del cierre no se asignaban a mis variables globales. @rintaro hizo un gran trabajo al explicar por qué este es el caso en una respuesta anterior.

Voy a incluir aquí un ejemplo general de cómo trabajé en esto. En mi caso, tuve varios arreglos globales que quería asignar dentro de un cierre, y luego hago algo cada vez (sin duplicar un montón de código).

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

@rintaro explicó perfectamente por qué no funciona, pero si realmente quieres hacerlo, usar UnsafeMutablePointer hará el truco:

 func getOneUserApiData(path: String, c: UnsafeMutablePointer) { 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() }