Trong quá trình phát triển các sản phẩm iOS, các bạn lập trình viên cần phải để ý tới việc số lượng bộ nhớ (amount of memory) mà app của chúng ta sử dụng. Nếu app quá phức tạp thì vấn đề về bộ nhớ là một trong những thách thức khó nhằn với các bạn lập trình viên. Chính vì lẽ đó, các bạn lập trình viên chúng ta phải rất cẩn thận với từng dòng code của mình và đương nhiên đặt ưu tiên hàng đâu cho việc tối ưu hoá cấp phát bộ nhớ. Chúng ta nên tránh việc khởi tạo hay làm gì đó vô tội vạ trừ khi điều đó rất là cần thiết, vì nó có thể gây khó khăn trong việc fix bug hay thậm chí là gây ảnh hưởng tới hiệu suất của app.

Trong Swift có một kỹ thuật gọi là lazy giúp cho bạn trì hoãn việc khởi tạo đối tượng cho đến khi nó thật sự được gọi đến. Nếu không được gọi đến, nó sẽ không bao giờ chạy.

Chúng ta dùng từ khoá lazy cho các đối tượng là biến (với từ khoá var), bởi vì giá trị ban đầu của nó không được lấy ra cho đến khi việc khởi tạo giá trị hoàn tất. Hằng số thì luôn luôn phải có giá trị trước khi việc khởi tạo hoàn tất nên không được khởi tạo với lazy.

Chúng ta tạo một struct là InterviewCandidate. Nó có 1 biến optional isiOS để kiểm tra là thí sinh này apply cho vị trí iOS hay Android. Hai biến iOSResumeDescription và androidResumeDescription để mô tả InterviewCandidate của thí sinh này được khai báo với từ khoá lazy. Nhìn vào dòng code dưới đây bạn sẽ hiểu, thí sinh apply ở vị trí iOS và biến giá trị lazy iOSResumeDescription sẽ được khởi tạo khi gọi print, còn biến androidResumeDescription sẽ bị nil cho đến khi nào nó được gọi thì mới khởi tạo giá trị.

import UIKit

struct InterviewCandidate {
    var isiOS:Bool?
    
    lazy var iOSResumeDescription: String = {
        return "I am an iOS developer"
    }()
    lazy var androidResumeDescription: String = {
        return "I am an android developer"
    }()
}

var candidateA = InterviewCandidate()
candidateA.isiOS = true

if candidateA.isiOS! {
    print(candidateA.iOSResumeDescription)
} else {
    print(candidateA.androidResumeDescription)
}

Đây là 1 ví dụ khá đơn giản. Nếu chúng ta có 1 class hay struct khá phức tạp chứ một đống biến computed và trả nó về giá trị từ 1 hàm đệ quy nào đó. Thử tưởng tượng bạn tạo khoảng 1000 objects như vậy thì chắc chắn 100% là vấn đề về performance và memory sẽ đập vào mặt bạn ngay.

Lazy Stored Property vs Stored Property

Có một vài lợi ích mà 1 biến lazy nhỉnh sơn so với các biến thông thường:

  1. Closure đi kèm với các biến lazy chỉ được thực thi chỉ khi bạn sử dụng biến ấy. Vì thế, nếu như vì vài lí do nào đó nó không được sử dụng (Vd: có thể là do quyết định của user giống như androidResumeDescription ở đoạn code phần trên cũng vậy) bạn sẽ tránh được việc phân bổ và khởi tạo không cần thiết cho app.
  2. Bạn có thể gán giá trị 1 một biến đã được lưu trữ vào một biến lazy
  3. Bạn có thể dùng self bên trong closure của biến lazy. Nếu như dùng self bên trong closure của lazy, nên để ý và khai báo thêm [unowned self] để tránh việc làm tăng strong reference count lên 1.
import UIKit
import Foundation

class InterviewTest {
    var name: String
    lazy var greeting : String = { [unowned self] in
        return "Hello \(self.name)"
    }()
    
    init(name: String) {
        self.name = name
    }
}
let testObj = InterviewTest(name:"abhi")
testObj.greeting

Bạn có thể tham chiếu các biến cho dù bạn có dùng closure hay không. Ví dụ:

lazy var iOSResumeDescription = “I am an iOS developer”

Note: Các bạn phải nhớ rằng, mục đích chính của biến lazy là nó chỉ được compiler thực thi khi nó được gọi ra, sau đó thì giá trị của nó đã được lưu lại. Chính vì lẽ đó, nếu bạn gọi iOSResumeDescription lần nữa, thì bạn vẫn sẽ thấy giá trị cũ trước đó.

Các quy tắc của Lazy

  • Bạn không thể dùng lazy với let
  • Bạn chỉ có thể dùng lazy bên trong struct và class
  • Biến lazy không được khởi tạo lúc ban đầu nên nó sẽ gây không an toàn cho thread.
  • Bạn không thể dùng lazy chung với computed properties. Bởi vì các computed properties thường trả về giá trị mỗi lần chúng ta truy cập tới sau khi chúng thực thi các dòng code bên trong block.