การปิดใช้พารามิเตอร์ที่ไม่หลบหนีอาจทำให้สามารถหลบหนีได้


151

ฉันมีโปรโตคอล:

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
นี่เป็นบทความที่ยอดเยี่ยมมากว่าทำไมมันถึงทำแบบนั้นใน Swift 3
Honey

1
มันไม่สมเหตุสมผลเลยที่เราต้องทำสิ่งนี้ ไม่มีภาษาอื่นต้องใช้
Andrew Koster

คำตอบ:


261

สาเหตุนี้เกิดจากการเปลี่ยนแปลงพฤติกรรมเริ่มต้นสำหรับพารามิเตอร์ประเภทฟังก์ชัน ก่อนที่จะใช้ 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
แต่คุณจะใช้วิธีปิดอย่างไรจึงไม่อนุญาตให้หลบหนี?
Eneko Alonso

6
@EnekoAlonso ไม่แน่ใจว่าคุณกำลังถามอะไร - คุณสามารถเรียกพารามิเตอร์ฟังก์ชันที่ไม่หลบหนีได้โดยตรงในฟังก์ชันเองหรือเรียกว่าเมื่อถูกจับในการปิดแบบไม่หลบหนี ในกรณีนี้ในขณะที่เรากำลังติดต่อกับรหัสตรงกันไม่มีการรับประกันว่าไม่มีasyncพารามิเตอร์ของฟังก์ชัน (และcompletionฟังก์ชั่น) จะถูกเรียกว่าก่อนที่จะfetchDataออก - @escapingและดังนั้นจึงต้อง
Hamish

รู้สึกน่าเกลียดที่เราต้องระบุ @escaping เป็น method signature สำหรับโปรโตคอล ... นั่นคือสิ่งที่เราควรทำหรือไม่? ข้อเสนอไม่พูด! : S
Sajjon

1
@Sajjon ปัจจุบันคุณไม่จำเป็นต้องจับคู่@escapingพารามิเตอร์ในข้อกำหนดโปรโตคอลกับ@escapingพารามิเตอร์ในการดำเนินการตามข้อกำหนดนั้น (และในทางกลับกันสำหรับพารามิเตอร์ที่ไม่หลบหนี) มันเหมือนกันใน Swift 2 สำหรับ@noescape.
Hamish


33

เนื่องจาก @noescape เป็นค่าเริ่มต้นจึงมี 2 ตัวเลือกในการแก้ไขข้อผิดพลาด:

1) ตามที่ @ แฮมิชชี้ให้เห็นในคำตอบของเขาเพียงแค่ทำเครื่องหมายความสมบูรณ์เป็น @escaping หากคุณสนใจเกี่ยวกับผลลัพธ์และต้องการให้มันหนีจริงๆ (นั่นอาจเป็นกรณีในคำถามของ @ Lukasz ที่มี Unit Tests เป็นตัวอย่างและความเป็นไปได้ของ async เสร็จสิ้น)

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

หรือ

2) รักษาพฤติกรรม @noescape เริ่มต้นโดยการทำให้สมบูรณ์เป็นทางเลือกทิ้งผลลัพธ์ทั้งหมดในกรณีที่คุณไม่สนใจผลลัพธ์ ตัวอย่างเช่นเมื่อผู้ใช้ "เดินจากไป" แล้วและตัวควบคุมมุมมองการโทรไม่จำเป็นต้องค้างในหน่วยความจำเพียงเพราะมีการโทรผ่านเครือข่ายโดยประมาท เช่นเดียวกับในกรณีของฉันเมื่อฉันมาที่นี่เพื่อหาคำตอบและโค้ดตัวอย่างก็ไม่เกี่ยวข้องกับฉันมากนักดังนั้นการทำเครื่องหมาย @noescape จึงไม่ใช่ตัวเลือกที่ดีที่สุดแม้ว่าจะฟังดูเป็นเพียงครั้งแรก

func fetchData(location: String, completion: ((DataFetchResult) -> Void)?) {
   ...
   completion?(self.result)
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.