El uso cerrado del parámetro de no escape puede permitirle escapar

139

Tengo un protocolo:

enum DataFetchResult {
    case success(data: Data)
    case failure
}

protocol DataServiceType {
    func fetchData(location: String, completion: (DataFetchResult) -> (Void))
    func cachedData(location: String) -> Data?
}

Con un ejemplo de implementación:

    /// An implementation of DataServiceType protocol returning predefined results using arbitrary queue for asynchronyous mechanisms.
    /// Dedicated to be used in various tests (Unit Tests).
    class DataMockService: DataServiceType {

        var result      : DataFetchResult
        var async       : Bool = true
        var queue       : DispatchQueue = DispatchQueue.global(qos: .background)
        var cachedData  : Data? = nil

        init(result : DataFetchResult) {
            self.result = result
        }

        func cachedData(location: String) -> Data? {
            switch self.result {
            case .success(let data):
                return data
            default:
                return nil
            }
        }

        func fetchData(location: String, completion: (DataFetchResult) -> (Void)) {

            // Returning result on arbitrary queue should be tested,
            // so we can check if client can work with any (even worse) implementation:

            if async == true {
                queue.async { [weak self ] in
                    guard let weakSelf = self else { return }

                    // This line produces compiler error: 
                    // "Closure use of non-escaping parameter 'completion' may allow it to escape"
                    completion(weakSelf.result)
                }
            } else {
               completion(self.result)
            }
        }
    }

El código anterior compiló y funcionó en Swift3 (Xcode8-beta5) pero ya no funciona con beta 6. ¿Me puede señalar la causa subyacente?

Lukasz
fuente
55
Este es un gran artículo sobre por qué se hace de esa manera en Swift 3
Honey
1
No tiene sentido que tengamos que hacer esto. Ningún otro idioma lo requiere.
Andrew Koster, el

Respuestas:

243

Esto se debe a un cambio en el comportamiento predeterminado para los parámetros del tipo de función. Antes de Swift 3 (específicamente la compilación que se envía con Xcode 8 beta 6), por defecto estarían escapando; tendría que marcarlos @noescapepara evitar que se almacenen o capturen, lo que garantiza que no durarán más que la duración de la llamada a la función.

Sin embargo, ahora @noescapees el valor predeterminado para los parámetros de tipo función. Si desea almacenar o capturar tales funciones, ahora debe marcarlas @escaping:

protocol DataServiceType {
  func fetchData(location: String, completion: @escaping (DataFetchResult) -> Void)
  func cachedData(location: String) -> Data?
}

func fetchData(location: String, completion: @escaping (DataFetchResult) -> Void) {
  // ...
}

Consulte la propuesta de Swift Evolution para obtener más información sobre este cambio.

Hamish
fuente
2
Pero, ¿cómo se usa un cierre para que no se escape?
Eneko Alonso
66
@EnekoAlonso No estoy completamente seguro de lo que está preguntando: puede llamar a un parámetro de función que no se escapa directamente en la función en sí, o puede llamarlo cuando se captura en un cierre que no se escapa. En este caso, como estamos tratando con código asincrónico, no hay garantía de que el asyncparámetro de función (y, por lo tanto, la completionfunción) se fetchDatainvoque antes de las salidas, y por lo tanto debe ser así @escaping.
Hamish
Se siente feo que tenemos que especificar @escaping como firma de método para protocolos ... ¿es eso lo que debemos hacer? La propuesta no dice! : S
Sajjon
1
@Sajjon Actualmente, debe hacer coincidir un @escapingparámetro en un requisito de protocolo con un @escapingparámetro en la implementación de ese requisito (y viceversa para los parámetros que no escapan). Fue lo mismo en Swift 2 para @noescape.
Hamish
@EnekoAlonso Vea developer.apple.com/documentation/swift/…
Peter Schorn
30

Como @noescape es el predeterminado, hay 2 opciones para corregir el error:

1) como señaló @Hamish en su respuesta, marque la finalización como @escaping si le importa el resultado y realmente quiere que se escape (ese es probablemente el caso en la pregunta de @ Lukasz con las Pruebas de Unidad como ejemplo y posibilidad de sincronización) terminación)

func fetchData(location: String, completion: @escaping (DataFetchResult) -> Void)

O

2) mantenga el comportamiento predeterminado de @noescape haciendo que la finalización sea opcional descartando los resultados por completo en los casos en que no le importe el resultado. Por ejemplo, cuando el usuario ya se ha "alejado" y el controlador de vista de llamada no tiene que quedarse en la memoria solo porque hubo una llamada de red descuidada. Tal como sucedió en mi caso cuando vine aquí en busca de respuesta y el código de muestra no era muy relevante para mí, por lo que marcar @noescape no fue la mejor opción, evento aunque sonó como el único a primera vista.

func fetchData(location: String, completion: ((DataFetchResult) -> Void)?) {
   ...
   completion?(self.result)
}
Vitalii
fuente