Skip to main content

Capture lists trong Swift: Tìm hiểu sự khác biệt giữa weak, strong, và unowned references


Giới thiệu

Như bạn đã biết, capture list được đặt ở trước danh sách parameter của một closure. Và giá trị của capture có thể là strong, weak hoặc unowned. Dường như mọi người đều sử dụng nó rất nhiều, chủ yếu nhằm tránh rơi vào vòng lặp của strong reference hay còn được gọi là retain cycles. Tuy nhiên khi mới bắt đầu, ta thường không biết nên sử dụng cái nào thì hợp lý, vì vậy, việc tìm hiểu sự khác nhau giữa strong, weak và unowned sẽ giúp ta có được quyết định đúng đắn.
Bây giờ hãy cùng điểm qua vấn đề mà ta sẽ đưa ra. Dưới đây là một class đơn giản với một phương thức:
class Kid {
    func eating() {
        print("Baby is eating")
    }
}
Bây giờ, hãy tạo ra một function, trong đó khởi tạo một instance của class Kid, đồng thời tạo một closure để gọi đến function eating(), rồi trả về closure này để sử dụng ở một nơi khác.
func eat() -> () -> Void {
    let moon = Kid()
    let eating = {
        moon.eating()
        return
    }
    return eating
}
Sau đó, ta có thể gọi eat() để nhận lại phương thức mà ta có thể gọi đến bất cứ khi nào ta muốn eating() được in ra.
let eatFood = eat()
eatFood()
Đoạn code trên sẽ in ra "Baby is eating".

Strong capturing

Trừ khi ta chỉ định những giá trị đặc biệt, Swift sẽ luôn sử dụng giá trị mặc định là strong capturing. Điều này đồng nghĩa với việc closure sẽ được capture với bất cứ external value nào được sử dụng bên trong thân của closure, và đảm bảo rằng chúng sẽ không bao giờ bị phá huỷ.
Hãy cùng nhìn lại func eat() mà ta đã khai báo ở trên:
func eat() -> () -> Void {
    let moon = Kid()
    let eating = {
        moon.eating()
        return
    }
    return eating
}
Instance "moon" mà ta tạo nằm bên trong function eat(), theo thường lệ, nó sẽ được huỷ khi function kết thúc. Tuy nhiên, bởi nó được sử dụng bên trong closure, nên Swift sẽ tự động giữ cho instance này luôn tồn tại chừng nào closure còn tồn tại, ngay cả khi function đã return.
Đây chính là strong capturing. Nếu Swift cho phép nó được huỷ, thì closure trên sẽ không còn an toàn để gọi tới, vì vậy moon.eating() sẽ không còn hợp lệ nữa.

Weak capturing

Khi set weak capturing, value sẽ không được giữ lại bởi closure, nó sẽ bị huỷ và set về nil. Vì vậy, những giá trị sử dụng weak capturing luôn là optional trong Swift. Hãy chỉnh lại ví dụ trên bằng weak capturing để xem sự khác biệt:
func eat() -> () -> Void {
    let moon = Kid()
    let eating = { [weak moon] in
        moon?.eating()
        return
    }
    return eating
}
Như vậy, [weak moon] là capture list, nó cũng là một phần của closure mà ta đã chỉ định là weak capturing. Và moon?.eating() bây giờ là một optional, nó có thể được set nil bất cứ lúc nào. Lúc này nếu bạn gọi eatFood(), nó sẽ không in ra giá trị gì, bởi moon chỉ tồn tại bên trong eat(), closure nó trả về không giữ strong reference tới nó.
Để hiểu rõ hành động này, hãy thử thay đổi như sau:
func eat() -> () -> Void {
    let moon = Kid()
    let eating = { [weak moon] in
        moon!.eating()
        return
    }
    return eating
}
Khi ta force unwraps instance moon bên trong closure, nó sẽ khiến đoạn code bị crash vì moon bị nil.

Unowned capturing

Unowned capturing có thể thay thế cho weak, nó giống như unwrapped optionals ngầm. Cũng giống như weak capturing, unowned capturing cho phép các giá trị bị nil tại bất cứ thời điểm nào trong tương lai. Tuy nhiên, ta có thể làm việc với chúng chừng nào chúng vẫn còn ở đó, và ta không cần phải unwrap optional. Hãy cùng xem ví dụ sau:
func eat() -> () -> Void {
    let moon = Kid()
    let eating = { [unowned moon] in
        moon.eating()
        return
    }
    return eating
}
Đoạn code trên vẫn sẽ crash như cách mà ta force unwrapped ở trên, unowned moon báo hiệu rằng nó biết chắc rằng moon sẽ luôn tồn tại trong suốt lifetime của closure mà nó trả về, vì thế nó không cần phải giữ reference trong memory, nhưng thực tế moon này sẽ bị huỷ ngay lập tức, dẫn đến đoạn code bị crash. Vì vậy ta cần nắm kỹ về unowned khi sử dụng để tránh gặp phải những lỗi không mong muốn.

Những sai lầm phổ biến

Thông thường ta hay mắc phải các sai lầm sau đây khi làm việc với closure capturing:
  • Không biết nên đặt capture list ở đâu khi closure có parameter
  • Tạo ra strong reference cycle, dẫn đến memory bị leak
  • Ta vô tình sử dụng strong references, đặc biệt khi sử dụng nhiều capture
  • Tạo ra bản copy của closure và chia sẻ captured data

Kết luận

Nếu ta biết chắc captured value của mình sẽ không bao giờ biến mất bất cứ khi nào closure được gọi, thì ta có thể sử dụng unowned. Điều này thật sự chỉ dành cho một số ít lần khi weak có thể gây ra phiền toái khi sử dụng, thậm chí ngay khi ta có thể sử dụng guard let bên trong closure với một biến weak capture.
Nếu ta gặp phải tình huống strong reference cycle, thì một trong hai phải sử dụng weak capturing.
Nếu ta đảm bảo rằng strong reference cycle sẽ không bao giờ xảy ra thì ta hoàn toàn có thể sử dụng strong capturing. Ví dụ, việc thực thi animation sẽ không làm self bị retain bên trong animation closure, vì thế strong capturing an toàn để sử dụng.
Tuy nhiên nếu ta không biết chắc nên sử dụng cái nào, thì hãy bắt đầu bằng weak và chỉ thay đổi khi nào cần thôi nhé.

Comments

Popular posts from this blog

Swift Tool Belt, Part 1: Adding a Border, Corner Radius, and Shadow to a UIView with Interface Builder

During my iOS work, I’ve assembled a set of code that I bring with me on every iOS project. I’m not talking about large frameworks or CocoaPods here. These are smaller Swift extensions or control overrides that are applicable to many projects. I think of them as my tool belt. In this post, I’ll show you an extension that will add a border, a corner radius, and a shadow to any UIView, UIButton, or UILabel and allow you to preview what it will look like in Interface Builder. Back in 2014, I wrote a blog post on Expanding User-Defined Runtime Attributes in Xcode where I added a border, corner radius, and shadow to a UIView using Interface Builder’s user-defined runtime attributes. This solution had no type checking—you had to type the property you wanted to modify by hand and often had to look up what it was called. You also had to run your project in order to see the effect of the runtime attribute. Starting with Xcode 6 , there is a new mech...

Alamofire vs URLSession

Alamofire vs URLSession: a comparison for networking in Swift Alamofire and URLSession both help you to make network requests in Swift. The URLSession API is part of the foundation framework, whereas Alamofire needs to be added as an external dependency. Many  developers  doubt  whether it’s needed to include an extra dependency on something basic like networking in Swift. In the end, it’s perfectly doable to implement a networking layer with the great URLSession API’s which are available nowadays. This blog post is here to compare both frameworks and to find out when to add Alamofire as an external dependency. Build better iOS apps faster Looking for a great mobile CI/CD solution that has tons of iOS-specific tools, smooth code signing, and even real device testing? Learn more about Bitrise’s iOS specific solutions! This shows the real power of Alamofire as the framework makes a lot of things easier. What is Alamofire? Where URLSession...

Frame vs Bounds in iOS

This article is a repost of an answer I wrote on Stack Overflow . Short description frame = a view’s location and size using the parent view’s coordinate system ( important for placing the view in the parent) bounds = a view’s location and size using its own coordinate system (important for placing the view’s content or subviews within itself) Details To help me remember frame , I think of a picture frame on a wall . The picture frame is like the border of a view. I can hang the picture anywhere I want on the wall. In the same way, I can put a view anywhere I want inside a parent view (also called a superview). The parent view is like the wall. The origin of the coordinate system in iOS is the top left. We can put our view at the origin of the superview by setting the view frame’s x-y coordinates to (0, 0), which is like hanging our picture in the very top left corner of the wall. To move it right, increase x, to move it down increase y. To help me remember bound...