Swift @escaping and Completion Handler


101

ฉันพยายามทำความเข้าใจ 'การปิด' ของ Swift ให้แม่นยำยิ่งขึ้น

แต่@escapingและCompletion Handlerยากเกินกว่าที่จะเข้าใจ

ฉันค้นหาการโพสต์และเอกสารอย่างเป็นทางการของ Swift มากมาย แต่ฉันรู้สึกว่ามันยังไม่เพียงพอ

นี่คือตัวอย่างรหัสของเอกสารราชการ

var completionHandlers: [()->Void] = []

func someFunctionWithEscapingClosure(completionHandler: @escaping ()->Void){
    completionHandlers.append(completionHandler)
}

func someFunctionWithNoneescapingClosure(closure: ()->Void){
    closure()
}

class SomeClass{
    var x:Int = 10
    func doSomething(){
        someFunctionWithEscapingClosure {
            self.x = 100
            //not excute yet
        }
        someFunctionWithNoneescapingClosure {
            x = 200
        }
    }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)

completionHandlers.first?() 
print(instance.x)

ฉันได้ยินมาว่ามีสองวิธีและเหตุผลที่ใช้ @escaping

อันดับแรกใช้สำหรับการจัดเก็บการปิดตัวที่สองใช้สำหรับวัตถุประสงค์ในการดำเนินงานของ Async

ต่อไปนี้เป็นคำถามของฉัน :

ขั้นแรกหากdoSomethingดำเนินการsomeFunctionWithEscapingClosureจะดำเนินการด้วยพารามิเตอร์การปิดและการปิดนั้นจะถูกบันทึกในอาร์เรย์ตัวแปรส่วนกลาง

ฉันคิดว่าการปิดคือ {self.x = 100}

วิธีselfใน {self.x = 100} ที่บันทึกไว้ในตัวแปรทั่วโลกcompletionHandlersสามารถเชื่อมต่อกับinstanceวัตถุที่SomeClass?

ประการที่สองฉันเข้าใจsomeFunctionWithEscapingClosureเช่นนี้

ในการจัดเก็บการปิดตัวแปรในเครื่องcompletionHandlerไปยังwe usingคำหลัก'completeHandlers @ escapeing` ของตัวแปรส่วนกลาง!

หากไม่มีการส่งคืน@escapingคำหลักsomeFunctionWithEscapingClosureตัวแปรภายในcompletionHandlerจะลบออกจากหน่วยความจำ

@escaping เก็บไว้ในความทรงจำ

นี่ใช่มั้ย?

สุดท้ายนี้ฉันแค่สงสัยเกี่ยวกับการมีอยู่ของไวยากรณ์นี้

บางทีนี่อาจเป็นคำถามพื้นฐาน

หากเราต้องการให้บางฟังก์ชันดำเนินการหลังจากฟังก์ชันเฉพาะบางฟังก์ชัน ทำไมเราไม่เรียกใช้ฟังก์ชันบางอย่างหลังจากเรียกฟังก์ชันเฉพาะ

อะไรคือความแตกต่างระหว่างการใช้รูปแบบด้านบนและการใช้ฟังก์ชันการเรียกกลับที่หลีกเลี่ยง

คำตอบ:


125

ตัวจัดการการเสร็จสิ้นอย่างรวดเร็วการหลบหนีและการไม่หลบหนี:

ดังที่ Bob Lee อธิบายไว้ในบล็อกโพสต์ของเขาตัวจัดการการเสร็จสมบูรณ์ใน Swift with Bob :

สมมติว่าผู้ใช้กำลังอัปเดตแอปขณะใช้งาน คุณต้องการแจ้งผู้ใช้อย่างแน่นอนเมื่อดำเนินการเสร็จสิ้น คุณอาจต้องการเปิดกล่องที่ระบุว่า "ยินดีด้วยตอนนี้คุณสนุกเต็มที่แล้ว!"

ดังนั้นคุณจะเรียกใช้บล็อกโค้ดหลังจากการดาวน์โหลดเสร็จสิ้นได้อย่างไร? นอกจากนี้คุณจะทำให้วัตถุบางอย่างเคลื่อนไหวได้อย่างไรหลังจากที่ตัวควบคุมมุมมองถูกย้ายไปที่ถัดไปแล้ว? เราจะมาดูวิธีออกแบบให้เหมือนเจ้านาย

จากรายการคำศัพท์ที่กว้างขวางของฉันตัวจัดการที่สมบูรณ์หมายถึง

ทำสิ่งต่างๆเมื่อทำสิ่งต่างๆเสร็จแล้ว

โพสต์ของ Bob ให้ความชัดเจนเกี่ยวกับตัวจัดการที่สมบูรณ์ (จากมุมมองของนักพัฒนามันกำหนดสิ่งที่เราต้องเข้าใจอย่างชัดเจน)

@escaping ปิด:

เมื่อมีการปิดในอาร์กิวเมนต์ของฟังก์ชันโดยใช้หลังจากที่ร่างกายของฟังก์ชันถูกเรียกใช้งานและส่งคืนคอมไพเลอร์ เมื่อฟังก์ชันสิ้นสุดลงขอบเขตของการปิดที่ผ่านไปจะมีอยู่และมีอยู่ในหน่วยความจำจนกว่าการปิดจะถูกดำเนินการ

มีหลายวิธีในการหลีกเลี่ยงการปิดในฟังก์ชันที่มี:

  • ที่เก็บข้อมูล: เมื่อคุณต้องการเก็บการปิดในตัวแปรส่วนกลางคุณสมบัติหรือหน่วยเก็บข้อมูลอื่น ๆ ที่มีอยู่ในหน่วยความจำที่ผ่านมาของฟังก์ชันการโทรจะถูกเรียกใช้งานและส่งคืนคอมไพเลอร์กลับ

  • การดำเนินการแบบอะซิงโครนัส: เมื่อคุณดำเนินการปิดแบบอะซิงโครนัสบนคิวการส่งออกคิวจะเก็บการปิดไว้ในหน่วยความจำสำหรับคุณซึ่งสามารถใช้ในอนาคตได้ ในกรณีนี้คุณไม่รู้ว่าจะดำเนินการปิดเมื่อใด

เมื่อคุณพยายามใช้การปิดในสถานการณ์เหล่านี้คอมไพเลอร์ Swift จะแสดงข้อผิดพลาด:

ภาพหน้าจอข้อผิดพลาด

เพื่อความชัดเจนมากขึ้นเกี่ยวกับหัวข้อนี้คุณสามารถตรวจสอบการโพสต์เกี่ยวกับเรื่องนี้ขนาดกลาง

เพิ่มอีกหนึ่งจุดซึ่งนักพัฒนา iOS ทุกคนต้องเข้าใจ:

  1. การปิดการหลบหนี : การปิดที่หลบหนีคือการปิดที่เรียกว่าหลังจากฟังก์ชันถูกส่งไปยังการส่งคืน กล่าวอีกนัยหนึ่งก็คือมันมีอายุยืนยาวกว่าฟังก์ชันที่ส่งผ่านไป
  2. การปิดแบบไม่หลบหนี : การปิดที่เรียกว่าภายในฟังก์ชันที่ถูกส่งผ่านไปเช่นก่อนที่จะกลับมา

@shabhakar จะเกิดอะไรขึ้นถ้าเราเก็บการปิด แต่ไม่เรียกมันในภายหลัง หรือถ้าเมธอดถูกเรียกสองครั้ง แต่เราเรียกว่าปิดเพียงครั้งเดียว เนื่องจากเรารู้ว่าผลลัพธ์นั้นเหมือนกัน
user1101733

@ user1101733 ฉันคิดว่าคุณกำลังพูดถึงการหนีการปิดการปิดจะไม่ดำเนินการจนกว่าคุณจะไม่โทร ในตัวอย่างข้างต้นหากเรียกเมธอด doSomething 2 คูณ 2 วัตถุ completeHandler จะเพิ่มในอาร์เรย์ completeHandlers หากคุณนำออบเจ็กต์แรกจากอาร์เรย์ completeHandlers และเรียกใช้มันจะดำเนินการ แต่จำนวนอาร์เรย์ completeHandlers จะยังคงเหมือนเดิม (2)
Deepak

@ ดีพัคใช่เรื่องการปิดหนี สมมติว่าเราไม่ได้ใช้อาร์เรย์และใช้ตัวแปรปกติเพื่อจัดเก็บข้อมูลอ้างอิงการปิดเนื่องจากเราจะเรียกใช้การเรียกล่าสุดอย่างไร หน่วยความจำบางส่วนจะถูกครอบครองโดยการปิดก่อนหน้านี้ซึ่งจะไม่มีวันโทร?
user1101733

1
@ user1101733 การปิดเป็นประเภทการอ้างอิง (เช่นคลาส) เมื่อคุณกำหนดการปิดใหม่ให้กับตัวแปรคุณสมบัติ / ตัวแปรจะชี้ไปที่การปิดใหม่ดังนั้น ARC จะยกเลิกการจัดสรรหน่วยความจำสำหรับการปิดครั้งก่อน
Deepak

29

นี่คือตัวอย่างเล็ก ๆ ที่ฉันใช้เพื่อเตือนตัวเองว่า @escaping ทำงานอย่างไร

class EscapingExamples: NSObject {

    var closure: (() -> Void)?

    func storageExample(with completion: (() -> Void)) {
        //This will produce a compile-time error because `closure` is outside the scope of this
        //function - it's a class-instance level variable - and so it could be called by any other method at
        //any time, even after this function has completed. We need to tell `completion` that it may remain in memory, i.e. `escape` the scope of this
        //function.
        closure = completion
        //Run some function that may call `closure` at some point, but not necessary for the error to show up.
        //runOperation()
    }

    func asyncExample(with completion: (() -> Void)) {
        //This will produce a compile-time error because the completion closure may be called at any time
        //due to the async nature of the call which precedes/encloses it.  We need to tell `completion` that it should
        //stay in memory, i.e.`escape` the scope of this function.
        DispatchQueue.global().async {
            completion()
        }
    }

    func asyncExample2(with completion: (() -> Void)) {
        //The same as the above method - the compiler sees the `@escaping` nature of the
        //closure required by `runAsyncTask()` and tells us we need to allow our own completion
        //closure to be @escaping too. `runAsyncTask`'s completion block will be retained in memory until
        //it is executed, so our completion closure must explicitly do the same.
        runAsyncTask {
            completion()
        }
    }





    func runAsyncTask(completion: @escaping (() -> Void)) {
        DispatchQueue.global().async {
            completion()
        }
    }

}

2
รหัสนี้ไม่ถูกต้อง ไม่มีการ@escapingคัดเลือก
Rob

ฉันชอบสิ่งนี้มากที่สุดi.e. escape the scope of this function.
Gal
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.