Penggunaan penutupan dari parameter non-escaping memungkinkannya untuk melarikan diri

151

Saya memiliki protokol:

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

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

Dengan contoh implementasi:

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

Kode di atas dikompilasi dan berfungsi di Swift3 (Xcode8-beta5) tetapi tidak berfungsi dengan beta 6 lagi. Bisakah Anda mengarahkan saya ke penyebab yang mendasarinya?

Lukasz
sumber
5
Ini adalah artikel yang sangat bagus tentang mengapa hal itu dilakukan seperti itu di Swift 3
Honey
1
Tidak masuk akal jika kami harus melakukan ini. Tidak ada bahasa lain yang membutuhkannya.
Andrew Koster

Jawaban:

261

Ini karena perubahan perilaku default untuk parameter tipe fungsi. Sebelum Swift 3 (khususnya build yang dikirimkan dengan Xcode 8 beta 6), mereka secara default akan melarikan diri - Anda harus menandai mereka @noescapeuntuk mencegah mereka disimpan atau ditangkap, yang menjamin mereka tidak akan hidup lebih lama dari durasinya dari pemanggilan fungsi.

Namun, sekarang @noescapeadalah default untuk parameter tipe fungsi. Jika Anda ingin menyimpan atau menangkap fungsi seperti itu, Anda sekarang perlu menandainya @escaping:

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

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

Lihat proposal Evolusi Swift untuk info lebih lanjut tentang perubahan ini.

Hamish
sumber
2
Tapi, bagaimana Anda menggunakan closure agar tidak bisa kabur?
Eneko Alonso
6
@EnekoAlonso Tidak sepenuhnya yakin apa yang Anda tanyakan - Anda bisa memanggil parameter fungsi non-escaping langsung di fungsinya itu sendiri, atau Anda bisa memanggilnya saat ditangkap dalam penutupan non-escaping. Dalam kasus ini, karena kita berurusan dengan kode asinkron, tidak ada jaminan bahwa asyncparameter fungsi (dan oleh karena itu completionfungsi) akan dipanggil sebelum fetchDatakeluar - dan oleh karena itu harus dipanggil @escaping.
Hamish
Terasa buruk bahwa kita harus menentukan @escaping sebagai tanda tangan metode untuk protokol ... apakah itu yang harus kita lakukan? Proposal tidak mengatakan! : S
Sajjon
1
@Sajjon Saat ini, Anda perlu mencocokkan @escapingparameter dalam persyaratan protokol dengan @escapingparameter dalam penerapan persyaratan itu (dan sebaliknya untuk parameter non-pelolosan). Itu sama di Swift 2 untuk @noescape.
Hamish
@EnekoAlonso Lihat developer.apple.com/documentation/swift/…
Peter Schorn
33

Karena @noescape adalah defaultnya, ada 2 opsi untuk memperbaiki kesalahan:

1) seperti yang ditunjukkan @Hamish dalam jawabannya, cukup tandai penyelesaian sebagai @escaping jika Anda benar-benar peduli dengan hasilnya dan benar-benar ingin hasilnya lolos (mungkin itu yang terjadi pada pertanyaan @ Lukasz dengan Unit Tests sebagai contoh dan kemungkinan asinkron penyelesaian)

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

ATAU

2) pertahankan perilaku @noescape default dengan membuat penyelesaian opsional dengan membuang hasil sama sekali jika Anda tidak peduli dengan hasilnya. Misalnya ketika pengguna sudah "pergi" dan pengontrol tampilan panggilan tidak harus menggantung di memori hanya karena ada beberapa panggilan jaringan yang ceroboh. Sama seperti kasus saya ketika saya datang ke sini untuk mencari jawaban dan kode sampelnya tidak terlalu relevan bagi saya, jadi menandai @noescape bukanlah pilihan terbaik, meskipun itu terdengar sebagai satu-satunya dari pandangan pertama.

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