L'uso di chiusura di un parametro non in escape può consentirne la fuga


139

Ho un protocollo:

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

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

Con un'implementazione di esempio:

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

Il codice sopra compilato e lavorato in Swift3 (Xcode8-beta5) ma non funziona più con beta 6. Puoi indicarmi la causa sottostante?


5
Questo è un ottimo articolo sul perché è stato fatto in questo modo in Swift 3
Honey, il

1
Non ha senso che dobbiamo farlo. Nessun'altra lingua lo richiede.
Andrew Koster,

Risposte:


243

Ciò è dovuto a una modifica del comportamento predefinito per i parametri del tipo di funzione. Prima di Swift 3 (in particolare la build fornita con Xcode 8 beta 6), per impostazione predefinita sarebbero in fuga: dovresti contrassegnarli @noescapeper impedire che vengano archiviati o catturati, il che garantisce che non sopravvivranno alla durata della chiamata di funzione.

Tuttavia, ora @noescapeè l'impostazione predefinita per i parametri digitati sulla funzione. Se si desidera memorizzare o acquisire tali funzioni, è ora necessario contrassegnarle @escaping:

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

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

Vedi la proposta Swift Evolution per maggiori informazioni su questa modifica.


2
Ma come si usa una chiusura in modo da non consentire la fuga?
Eneko Alonso,

6
@EnekoAlonso Non sei del tutto sicuro di ciò che stai chiedendo: puoi chiamare un parametro di funzione non di escape direttamente nella funzione stessa oppure puoi chiamarlo quando catturato in una chiusura di non escape. In questo caso, dato che abbiamo a che fare con il codice asincrono, non vi è alcuna garanzia che il asyncparametro della funzione (e quindi la completionfunzione) verrà chiamato prima delle fetchDatauscite - e quindi deve esserlo @escaping.
Hamish,

Sembra brutto che dobbiamo specificare @escaping come firma del metodo per i protocolli ... è quello che dovremmo fare? La proposta non dice! : S
Sajjon,

1
@Sajjon Attualmente, è necessario abbinare un @escapingparametro in un requisito di protocollo con un @escapingparametro nell'implementazione di tale requisito (e viceversa per i parametri non di escape). Era lo stesso in Swift 2 per @noescape.
Hamish,


30

Poiché @noescape è l'impostazione predefinita, sono disponibili 2 opzioni per correggere l'errore:

1) come ha sottolineato @Hamish nella sua risposta, basta contrassegnare il completamento come @escaping se ti interessa il risultato e vuoi davvero che scappi (questo è probabilmente il caso nella domanda di @ Lukasz con Test delle unità come esempio e possibilità di asincrono completamento)

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

O

2) mantieni il comportamento predefinito di @noescape rendendo facoltativo il completamento scartando del tutto i risultati nei casi in cui non ti interessa il risultato. Ad esempio, quando l'utente ha già "abbandonato" e il controller della vista chiamante non deve rimanere in memoria solo perché c'era una chiamata di rete trascurata. Proprio come nel mio caso quando sono venuto qui in cerca di risposta e il codice di esempio non era molto pertinente per me, così contrassegnare @noescape non era l'opzione migliore, anche se sembrava essere l'unico dal primo sguardo.

func fetchData(location: String, completion: ((DataFetchResult) -> Void)?) {
   ...
   completion?(self.result)
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.