La fermeture d'un paramètre non échappant peut lui permettre de s'échapper

139

J'ai un protocole:

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

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

Avec un exemple d'implémentation:

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

Le code ci-dessus compilé et fonctionnait en Swift3 (Xcode8-beta5) mais ne fonctionne plus avec beta 6. Pouvez-vous m'indiquer la cause sous-jacente?

Lukasz
la source
5
C'est un très bon article sur les raisons pour lesquelles c'est fait de cette façon dans Swift 3
Honey
1
Cela n'a aucun sens que nous devions faire cela. Aucune autre langue ne l'exige.
Andrew Koster

Réponses:

243

Cela est dû à une modification du comportement par défaut des paramètres de type de fonction. Avant Swift 3 (en particulier la version livrée avec Xcode 8 beta 6), ils s'échappaient par défaut - vous devrez les marquer @noescapepour éviter qu'ils ne soient stockés ou capturés, ce qui garantit qu'ils ne survivront pas à la durée. de l'appel de fonction.

Cependant, c'est maintenant @noescapela valeur par défaut pour les paramètres de type fonction. Si vous souhaitez stocker ou capturer de telles fonctions, vous devez maintenant les marquer @escaping:

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

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

Consultez la proposition Swift Evolution pour plus d'informations sur ce changement.

Hamish
la source
2
Mais, comment utiliser une fermeture pour qu'elle ne permette pas de s'échapper?
Eneko Alonso
6
@EnekoAlonso Pas tout à fait sûr de ce que vous demandez - vous pouvez soit appeler un paramètre de fonction non d'échappement directement dans la fonction elle-même, soit l'appeler lorsqu'il est capturé dans une fermeture non d'échappement. Dans ce cas, comme nous avons affaire à du code asynchrone, il n'y a aucune garantie que le asyncparamètre de fonction (et donc la completionfonction) sera appelé avant les fetchDatasorties - et doit donc l'être @escaping.
Hamish
C'est moche que nous devions spécifier @escaping comme signature de méthode pour les protocoles ... est-ce que nous devrions faire? La proposition ne dit pas! : S
Sajjon
1
@Sajjon Actuellement, vous devez faire correspondre un @escapingparamètre dans une exigence de protocole avec un @escapingparamètre dans l'implémentation de cette exigence (et vice versa pour les paramètres sans échappement). C'était la même chose dans Swift 2 pour @noescape.
Hamish
@EnekoAlonso Voir developer.apple.com/documentation/swift/…
Peter Schorn
30

Puisque @noescape est la valeur par défaut, il existe 2 options pour corriger l'erreur:

1) comme @Hamish l'a souligné dans sa réponse, marquez simplement la fin comme @escaping si vous vous souciez du résultat et que vous voulez vraiment qu'il s'échappe (c'est probablement le cas dans la question de @ Lukasz avec les tests unitaires comme exemple et la possibilité d'async achèvement)

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

OU

2) Conservez le comportement par défaut @noescape en rendant la complétion facultative en supprimant complètement les résultats dans les cas où vous ne vous souciez pas du résultat. Par exemple, lorsque l'utilisateur s'est déjà "éloigné" et que le contrôleur de vue appelant n'a pas à se bloquer en mémoire simplement parce qu'il y a eu un appel réseau imprudent. Tout comme c'était le cas dans mon cas lorsque je suis venu ici pour chercher une réponse et que l'exemple de code n'était pas très pertinent pour moi, donc marquer @noescape n'était pas la meilleure option, même si cela semblait être le seul du premier coup d'œil.

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