Die Verwendung eines nicht-Escape-Parameters kann das Schließen ermöglichen

139

Ich habe ein Protokoll:

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

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

Mit einer Beispielimplementierung:

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

Der obige Code wurde in Swift3 (Xcode8-beta5) kompiliert und funktioniert, funktioniert aber nicht mehr mit Beta 6. Können Sie mich auf die zugrunde liegende Ursache hinweisen?

Lukasz
quelle
5
Dies ist ein sehr guter Artikel darüber, warum es in Swift 3
Honey
1
Es macht keinen Sinn, dass wir das tun müssen. Keine andere Sprache erfordert es.
Andrew Koster

Antworten:

243

Dies ist auf eine Änderung des Standardverhaltens für Parameter des Funktionstyps zurückzuführen. Vor Swift 3 (speziell dem Build, der mit Xcode 8 Beta 6 @noescapegeliefert wird ) werden sie standardmäßig entkommen - Sie müssten sie markieren, um zu verhindern, dass sie gespeichert oder erfasst werden, was garantiert, dass sie die Dauer nicht überleben des Funktionsaufrufs.

Dies ist jedoch jetzt @noescapedie Standardeinstellung für funktionsbezogene Parameter. Wenn Sie solche Funktionen speichern oder erfassen möchten, müssen Sie sie jetzt markieren @escaping:

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

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

Weitere Informationen zu dieser Änderung finden Sie im Swift Evolution-Vorschlag .

Hamish
quelle
2
Aber wie benutzt man einen Verschluss, damit er nicht entkommen kann?
Eneko Alonso
6
@EnekoAlonso Sie sind sich nicht ganz sicher, was Sie fragen - Sie können entweder einen nicht-Escape-Funktionsparameter direkt in der Funktion selbst aufrufen oder ihn aufrufen, wenn er in einem nicht-Escape-Closure erfasst wird. In diesem Fall gibt es, da es sich um asynchronen Code handelt, keine Garantie dafür, dass der asyncFunktionsparameter (und damit die completionFunktion) vor dem fetchDataBeenden aufgerufen wird - und dies auch sein muss @escaping.
Hamish
Es ist hässlich, dass wir @escaping als Methodensignatur für Protokolle angeben müssen ... sollten wir das tun? Der Vorschlag sagt nicht! : S
Sajjon
1
@Sajjon Derzeit müssen Sie einen @escapingParameter in einer Protokollanforderung mit einem @escapingParameter in der Implementierung dieser Anforderung abgleichen (und umgekehrt für nicht maskierende Parameter). In Swift 2 war es dasselbe für @noescape.
Hamish
@EnekoAlonso Siehe developer.apple.com/documentation/swift/…
Peter Schorn
30

Da @noescape die Standardeinstellung ist, gibt es zwei Möglichkeiten, um den Fehler zu beheben:

1) Wie @Hamish in seiner Antwort hervorhob, markieren Sie die Fertigstellung einfach als @escaping, wenn Sie sich für das Ergebnis interessieren und wirklich möchten, dass es entweicht (dies ist wahrscheinlich in @ Lukasz 'Frage mit Unit Tests als Beispiel und Möglichkeit der Asynchronisation der Fall Fertigstellung)

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

ODER

2) Behalten Sie das Standardverhalten von @noescape bei, indem Sie die Fertigstellung optional machen und die Ergebnisse insgesamt verwerfen, wenn Sie sich nicht um das Ergebnis kümmern. Zum Beispiel, wenn der Benutzer bereits "weggegangen" ist und der Controller der aufrufenden Ansicht nicht im Speicher hängen bleiben muss, nur weil ein unachtsamer Netzwerkanruf stattgefunden hat. Genau wie in meinem Fall, als ich hierher kam, um nach einer Antwort zu suchen, und der Beispielcode für mich nicht sehr relevant war, war das Markieren von @noescape nicht die beste Option, obwohl es auf den ersten Blick als das einzige klang.

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