@escaping và @nonescaping closure trong Swift

Trong quá trình code iOS với Swift, chắc chắn nhiều lần bạn đã gặp keyword @escaping hoặc @nonescaping khi làm việc với closure. Đã bao giờ bạn tự hỏi ý nghĩa của hai keyword này là gì chưa?

Trong bài viết này, chúng ta hãy cùng tìm hiểu về hai khái niệm này.

Closure

Trước hết hãy nhắc lại một chút về khái niệm closure. Định nghĩa của Apple về closure như sau:

Closures are self-contained blocks of functionality that can be passed around and used in your code.

Như vậy, closure là các block code được đặt trong cặp dấu đóng ngoặc nhọn { } thực hiện một chức năng nào đó, được truyền vào làm parameter của function hoặc được sử dụng linh hoạt trong code.

Ví dụ về một closure đơn giản, nhận một param kiểu String và in ra lời chào:

let hello: (String) -> Void = { fullName in
    print("Bonjour \(fullName)")
}

hello("Harry Style") // "Bonjour Harry Style"

Ví dụ về closure được dùng làm parameter của function:

func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
    // Logical code to dismiss
    completion?()
}

dismiss(animated: true) {
     self.navigationController?.navigationBar.isHidden = true   
}

@nonescaping closure

Trong Swift 1 và Swift 2, các closure được truyền vào làm parameter của các function mặc định được coi là @escaping. Nghĩa là closure đó sẽ không escape sau khi function đó thực hiện xong. Ngược lại, nếu chúng ta set closure đó là @nonescaping thì khi kết thúc function body, closure đó sẽ được giải phóng khỏi bộ nhớ.

Tuy nhiên, từ Swift 3 trở đi, Apple đã thay đổi: mặc định closure là non-escaping, nếu muốn giữ lại closure sau khi function kết thúc, chúng ta phải thêm keyword @escaping trước closure parameter đó.

Khi closure được sử dụng làm function parameter, khi function đó được excute, closure sẽ được excute và trả về kết quả. Đến cuối function body, khi gặp dấu đóng ngoặc nhọn, function kết thúc, closure được truyền vào đó bị out of scope và bị giải phóng khỏi bộ nhớ.

Life cycle của một @nonescaping closure rất đơn giản:

  1. Closure được pass vào function argument.
  2. Function excute closure. Closure được giải phóng khỏi bộ nhớ.
  3. Function return.
func getSumOf(numbers: [Int], completion: (Int) -> Void) {
    // 2. Excute function.
    var sum = 0
    for aNumber in numbers {
        sum += aNumber
    }
       
    // 3. Function excute closure.
    completion(sum)
}
    
func doSomething() {
    // 1. Gọi function, truyền closure vào làm tham số.
    getSumOf(numbers: [34, 16, 231, 6 , 23, -83]) { result in
        print("Sum is \(result)")
        // 4. Closure được excute xong, return compiler và closure out of scope, bị giải phóng khỏi bộ nhớ.
    }
}

Mặc định từ Swift 3, các closure đều là @nonescaping vì tính tối ưu và dễ dàng quản lý bộ nhớ hơn. Khi sử dụng @nonescaping closure, compiler cũng không cần reference tới self, không xảy ra trường hợp reference cycle. Vì vậy không cần sử dụng [weak self] hoặc [unowned self].

class Child {
    
    func doTheThing(closure: () -> Void) {
        closure()
    }

}
class Parent {
    
    var child = Child()
    var name = "Ronny"
    func doStuff() {
        child.doTheThing {
            // Không cần self.name
            name = "Steven"
        }
    }

}

Như ví dụ trên compiler sẽ không yêu cầu phải self. reference, không xảy ra reference cycle vì closure là non-escaping và nó bị hủy khỏi bộ nhớ ngày khi function chứa nó doTheThing() kết thúc.

@escaping closure

Khi truyền closure làm function argument, có những trường hợp mà closure sẽ được giữ lại để excute sau khi function được excute và trả lại compiler. Lúc này, closure không bị out of scope và vẫn được giữ lại trong bộ nhớ. Hai trường hợp thường thấy là:

  • Khi cần giữ lại closure thành property của class để chờ excute sau. Để closure có thể được giữ lại trong bộ nhớ sau khi function excute xong và return compiler. Ví dụ: chờ response API...
var complitionHandler: ((String) -> Void)?

func requestURL(urlString: String, handler: @escaping (String) -> Void) {
    // 2. Excute function.
    var response = ""
    // Call API code
    ...

    // 3. Closure được gán lại thành property, không bị out of scope
    complitionHandler = handler
}

func loadData() {
    // 1. Gọi function, truyền closure vào làm tham số.
    requestURL(urlString:"https://mocky.io/api/v1/accounts") { response in
        // Handle response string code
        ...
        // 4. Closure được excute xong, return compiler, nhưng vẫn được giữ lại trong bộ nhớ.
    }

}
  • Khi excute closure trên một thread khác, một asynchronous dispatch queue. Queue này sẽ giữ closure trong bộ nhớ, nhằm excute sau. Trường hợp này, chúng ta sẽ không biết được closure sẽ được excute chính xác vào thời điểm nào.
func getSumOf(numbers: [Int], completion: @escaping (Int) -> Void) {
    // 2. Excute function.
    var sum = 0
    for aNumber in numbers {
        sum += aNumber
    }

    // Delay 5s và excute closure trên global queue.
    DispatchQueue.glonal.asyncAfter(deadline: .now() + 5) {
        completion(sum)
    }

}
func doSomething() {
    // 1. Gọi function, truyền closure vào làm tham số.
    getSumOf(numbers: [34, 16, 231, 6 , 23, -83]) { result in
        print("Sum is \(result)")
        // 4. Closure được excute xong, return compiler và closure chưa bị giải phóng vì đang được giữ lại trên queue khác để excute sau.
    }

}