O uso de fechamento de parâmetro não escapante pode permitir que ele escape

139

Eu tenho um protocolo:

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

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

Com um exemplo de implementação:

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

O código acima compilou e funcionou no Swift3 (Xcode8-beta5), mas não funciona mais com o beta 6. Você pode me indicar a causa subjacente?

Lukasz
fonte
5
Este é um artigo muito bom sobre por que isso foi feito em Swift 3
Honey
1
Não faz sentido que tenhamos que fazer isso. Nenhum outro idioma exige isso.
Andrew Koster

Respostas:

243

Isso ocorre devido a uma alteração no comportamento padrão dos parâmetros do tipo de função. Antes do Swift 3 (especificamente a compilação que acompanha o Xcode 8 beta 6), eles teriam como padrão escapar - você teria que marcá-los @noescapepara impedir que eles fossem armazenados ou capturados, o que garante que eles não sobreviverão à duração da chamada de função.

No entanto, agora @noescapeé o padrão para parâmetros do tipo de função. Se você deseja armazenar ou capturar essas funções, agora precisa marcá-las @escaping:

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

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

Consulte a proposta do Swift Evolution para obter mais informações sobre essa alteração.

Hamish
fonte
2
Mas como você usa um fechamento para que ele não escape?
Eneko Alonso
6
@EnekoAlonso Não tem muita certeza do que está perguntando - você pode chamar um parâmetro de função não escapatória diretamente na própria função ou pode chamá-lo quando capturado em um fechamento não escapante. Nesse caso, como estamos lidando com código assíncrono, não há garantia de que o asyncparâmetro da função (e, portanto, a completionfunção) seja chamado antes das fetchDatasaídas - e, portanto, deve ser @escaping.
Hamish
Parece horrível que tenhamos que especificar @escaping como assinatura de método para protocolos ... é isso que devemos fazer? A proposta não diz! : S
Sajjon 19/09/16
1
@Sajjon Atualmente, você precisa combinar um @escapingparâmetro em um requisito de protocolo com um @escapingparâmetro na implementação desse requisito (e vice-versa para parâmetros sem escape). Foi o mesmo em Swift 2 para @noescape.
Hamish
@EnekoAlonso Veja developer.apple.com/documentation/swift/…
Peter Schorn
30

Como @noescape é o padrão, existem 2 opções para corrigir o erro:

1) como @Hamish apontou em sua resposta, marque a conclusão como @escaping se você se importa com o resultado e realmente quer que ele escape (esse provavelmente é o caso na pergunta de @ Lukasz com testes de unidade como exemplo e possibilidade de assíncrona conclusão)

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

OU

2) mantenha o comportamento padrão do @noescape, tornando opcional a conclusão descartando os resultados completamente nos casos em que você não se importa com o resultado. Por exemplo, quando o usuário já "se afastou" e o controlador de exibição de chamada não precisa travar na memória apenas porque houve uma chamada de rede descuidada. Assim como no meu caso, quando cheguei aqui, procurando por respostas e o código de exemplo não era muito relevante para mim, marcar o @noescape não era a melhor opção, apesar de soar como o único à primeira vista.

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