Закрытие использования неэкранирующего параметра может позволить ему экранировать

139

У меня есть протокол:

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

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

С примером реализации:

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

Приведенный выше код скомпилирован и работает в Swift3 (Xcode8-beta5), но больше не работает с бета 6. Можете ли вы указать мне на причину?

Лукаш
источник
5
Это очень
Honey
1
Нет смысла, что мы должны это делать. Ни один другой язык не требует этого.
Эндрю Костер

Ответы:

243

Это связано с изменением поведения по умолчанию для параметров типа функции. До Swift 3 (в частности, сборки, поставляемой с Xcode 8 beta 6), по умолчанию они будут экранироваться - вам нужно будет пометить их @noescape, чтобы предотвратить их сохранение или захват, что гарантирует, что они не переживут длительность вызова функции.

Тем не менее, теперь @noescapeэто значение по умолчанию для параметров с типом функции. Если вы хотите сохранить или записать такие функции, вам нужно пометить их @escaping:

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

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

См. Предложение Swift Evolution для получения дополнительной информации об этом изменении.

Хэмиш
источник
2
Но как вы используете закрытие, чтобы оно не позволяло сбежать?
Энеко Алонсо
6
@EnekoAlonso Не совсем уверен, что вы спрашиваете - вы можете вызвать параметр неэкранирующей функции непосредственно в самой функции, или вы можете вызвать его, если он захвачен в неэкранирующем закрытии. В этом случае, поскольку мы имеем дело с асинхронным кодом, нет гарантии, что asyncпараметр функции (и, следовательно, completionфункция) будет вызван до fetchDataвыхода - и, следовательно, должен быть @escaping.
Хэмиш
Кажется уродливым, что мы должны указать @escaping в качестве сигнатуры метода для протоколов ... это то, что мы должны сделать? Предложение не говорит! : S
Саджон
1
@Sajjon В настоящее время вам необходимо сопоставить @escapingпараметр в требовании протокола с @escapingпараметром в реализации этого требования (и наоборот для неэкранирующих параметров). То же самое было в Swift 2 для @noescape.
Хэмиш
@EnekoAlonso См developer.apple.com/documentation/swift/...
Питер Schorn
30

Поскольку @noescape используется по умолчанию, есть 2 варианта исправления ошибки:

1) как указал @Hamish в своем ответе, просто пометьте завершение как @escaping, если вы действительно заботитесь о результате и действительно хотите его избежать (это, вероятно, имеет место в вопросе @ Lukasz с юнит-тестами в качестве примера и возможностью асинхронности) завершение)

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

ИЛИ

2) сохранить стандартное поведение @noescape, сделав необязательное завершение необязательным, полностью отбрасывая результаты в тех случаях, когда вы не заботитесь о результате. Например, когда пользователь уже "ушел" и контроллер вызывающего представления не должен зависать в памяти только потому, что произошел какой-то небрежный сетевой вызов. Так же, как и в моем случае, когда я пришел сюда в поисках ответа, и пример кода не был очень уместен для меня, поэтому пометка @noescape была не лучшим вариантом, хотя с первого взгляда это звучало как единственное.

func fetchData(location: String, completion: ((DataFetchResult) -> Void)?) {
   ...
   completion?(self.result)
}
Vitalii
источник