Zamknięcie parametru bez ucieczki może pozwolić mu na ucieczkę

139

Mam protokół:

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

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

Z przykładową realizacją:

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

Powyższy kod został skompilowany i działał w Swift3 (Xcode8-beta5), ale nie działa już z wersją beta 6. Czy możesz wskazać mi przyczynę?

Łukasz
źródło
5
To jest bardzo świetny artykuł o tym, dlaczego tak się dzieje w Swift 3
Honey
1
Nie ma sensu, że musimy to robić. Żaden inny język tego nie wymaga.
Andrew Koster

Odpowiedzi:

243

Wynika to ze zmiany domyślnego zachowania parametrów typu funkcji. Przed Swift 3 (w szczególności kompilacją dostarczaną z Xcode 8 beta 6), domyślnie uciekały - musiałbyś je oznaczyć @noescape, aby zapobiec ich przechowywaniu lub przechwyceniu, co gwarantuje, że nie przetrwają wywołania funkcji.

Jednak teraz @noescapejest wartością domyślną dla parametrów o typie funkcji. Jeśli chcesz przechowywać lub przechwytywać takie funkcje, musisz je teraz zaznaczyć @escaping:

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

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

Zobacz propozycję Swift Evolution, aby uzyskać więcej informacji na temat tej zmiany.

Hamish
źródło
2
Ale jak używać zamknięcia, aby nie pozwalało uciec?
Eneko Alonso,
6
@EnekoAlonso Nie jestem do końca pewien, o co pytasz - możesz albo wywołać parametr funkcji bez zmiany znaczenia bezpośrednio w samej funkcji lub możesz wywołać go po przechwyceniu w zamknięciu bez ucieczki. W tym przypadku, ponieważ mamy do czynienia z kodem asynchronicznym, nie ma gwarancji, że asyncparametr funkcji (a tym samym completionfunkcja) zostanie wywołana przed zakończeniem fetchData- i dlatego musi być @escaping.
Hamish,
To brzydkie, że musimy określić @escaping jako sygnaturę metody dla protokołów ... czy to właśnie powinniśmy zrobić? Wniosek nie mówi! : S
Sajjon
1
@Sajjon Obecnie musisz dopasować @escapingparametr w wymaganiu protokołu z @escapingparametrem w implementacji tego wymagania (i odwrotnie w przypadku parametrów bez zmiany znaczenia). Tak samo było w Swift 2 dla @noescape.
Hamish
@EnekoAlonso Zobacz developer.apple.com/documentation/swift/…
Peter Schorn
30

Ponieważ @noescape jest ustawieniem domyślnym, istnieją 2 opcje naprawienia błędu:

1) jak @Hamish wskazał w swojej odpowiedzi, po prostu zaznacz ukończenie jako @escaping, jeśli zależy ci na wyniku i naprawdę chcesz, aby uciekł (tak prawdopodobnie jest w pytaniu @ Łukasza z testami jednostkowymi jako przykładem i możliwością asynchronizacji ukończenie)

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

LUB

2) zachowaj domyślne zachowanie @noescape, ustawiając uzupełnianie jako opcjonalne, całkowicie odrzucając wyniki w przypadkach, gdy nie dbasz o wynik. Na przykład, gdy użytkownik już „odszedł” i wywołujący kontroler widoku nie musi zawieszać się w pamięci tylko z powodu nieostrożnego połączenia sieciowego. Tak jak to było w moim przypadku, kiedy przyszedłem tutaj, szukając odpowiedzi, a przykładowy kod nie był dla mnie zbyt istotny, więc oznaczenie @noescape nie było najlepszą opcją, chociaż brzmiało to jako jedyne na pierwszy rzut oka.

func fetchData(location: String, completion: ((DataFetchResult) -> Void)?) {
   ...
   completion?(self.result)
}
Vitalii
źródło