Smart KeyPaths: tối ưu Key-Value Coding cho Swift

Swift đã chính thức được ra mắt cùng với khá nhiều tính năng mới. Có 1 phần chức năng mới mà mọi người ít để ý đến: KeyPaths. Nó có khá nhiều điều thú vị để khám phá mà trước đó tôi không nhận ra.
KeyPath là 1 cách an toàn riêng để truy vấn đế 1 thuộc tính và lấy kết quả. Bạn hoàn toàn có thể làm điều đó ở trên Swift 3, nhưng với Swift 4 bạn không thể làm thế mà không thể làm vậy với các thuộc tính mà không gói chúng vào 1 closure, hoặc sử dụng các function cũ không an toàn (#keyPath()):

Với phong cách code của Swift 3 (phong cách thôi nhá, vẫn chạy Swift 4 đó 😁 ):

@objcMembers class Kid : NSObject {
    dynamic var nickname: String = ""
    dynamic var age: Int = 0

    init(nickname: String, age: Int) {
        self.nickname = nickname
        self.age = age
    }
}

var ben = Kid(nickname: "Benji", age: 0)
let kidsNameKeyPath = #keyPath(Kid.nickname)
let name = ben.value(forKeyPath: kidsNameKeyPath)
ben.setValue("Ben", forKeyPath: kidsNameKeyPath)

ben.nickname

tức là để gọi đến 1 keyPath chúng ta sử dụng cú pháp: “#keyPath(Kid.nickname)”

nếu bạn để ý thì tuy chúng ta viết theo cú pháp gọi đến properties nhưng nó vẫn chỉ là 1 String => không an toàn

Còn với hàm “ben.value(forKeyPath: kidsNameKeyPath)” thì return Any => không an toàn

=> Để tối ưu KeyPath cần phải làm được những điều sau:

  • Property traversal
  • Statically type-safe
  • Fast
  • Applicable to all values
  • Works on all platforms

Và đây là thành quả mà đội ngũ Foudation developer đã làm được:

  1. Chúng được bắt đầu với dấu gạch chéo ngược, theo sau là BaseType, dấu chấm cho biết chúng ta đang làm gì đó bên trong BaseType đó, và sau đó là tên của Property. Và dấu gạch chéo ở đây rất quan trọng bởi vì nó giúp chúng ta phân biệt được việc gọi đến KeyPath hay gọi đến Property như bình thường.
  1. Trong trường hợp chúng ta không biết BaseType, chúng ta có thể bỏ BaseType và viết như sau:
  1. Chúng ta vẫn có thể truy cập và các property của property của property như trước:
  1. Ngoài ra nó còn có thể hoạt động với Optional chaining:
  1. Và nó cũng sẽ cho phép truy cập thông qua Subscript:
  1. KeyPath cũng có thể bắt đầu với 1 Subscript:
  1. Nhìn cú pháp ở trên các bạn thấy nó khá tuyệt đúng không? Vậy làm thế nào để có thể sử dụng chúng? Đơn giản thôi:

Cùng đi sâu vào xem điều thú vị nhé:

Các loại KeyPath

AnyKeyPath
    |
    v
PartialKeyPath<Root>
    |
    v
KeyPath<Root, Value>
    |
    v
WritableKeyPath<Root, Value>
    |
    v
ReferenceWritableKeyPath<Root, Value> 

KeyPath<Root, Value> là keyPath của những property được khai báo là let
WritableKeyPath<Root, Value> là những keyPath của property được khai báo là var, tức là chúng ta có thể sử dụng keyPath để set lại giá trị cho property.
ReferenceWritableKeyPath<Root, Value> được kế thừa từ WritableKeyPath<Root, Value>, là 1 key path hỗ trợ đọc và sửa property được tham chiếu đến
**PartialKeyPath<Root> ** là 1 loại keyPath có BaseType mà không có đường dẫn cụ thể được định nghĩa
AnyKeyPath được sử dụng trong trường hợp chúng ta không biết kiểu dữ liệu của BaseType khi code.

Về cách sử dụng

Thử ví dụ dưới đây

struct Kid {
    let nickname: String = ""
    let age: Double = 0.0
}

let nicknameKeyPath = Kid.nickname
let ageKeyPath = Kid.age

Nhấn Option Click chuột trái vào “nicknameKeyPath” để xem kiểu dữ liệu ta sẽ thấy, đó là 1 KeyPath với BaseType là Kid và Property Type là String

=> với KeyPath mới kiểu dữ liệu của chúng đã được định nghĩa 1 cách rõ ràng và an toàn

Ứng dụng Key Value Observing

bạn hãy chạy thử example dưới đây trên playground để thấy sự thuận tiện của keyPath mới nhé

@objcMembers class Kid : NSObject {
    dynamic var nickname: String = ""
    dynamic var age: Int = 0

    init(nickname: String, age: Int) {
        self.nickname = nickname
        self.age = age
    }

    // Mỗi lần sinh nhật cho thêm 1 tuổi
    func celebrateBirthday() {
        age += 1
    }
}

let ben = Kid(nickname: "Benji", age: 5)

@objcMembers class KindergartenController : NSObject {
    dynamic var representedKid: Kid
    var ageObservation: NSKeyValueObservation?

    init(kid: Kid) {
        representedKid = kid
        super.init()
        addObserver()
    }

    func addObserver() {
        ageObservation = observe(KindergartenController.representedKid.age) { observed, change in
            print("(observed.representedKid.nickname) thêm một tuổi: (observed.representedKid.age)")
            if observed.representedKid.age >= 18 {
                print("Haha 18 rồi éo phải kid nữa đâu")
            }
        }
    }
}

let controller = KindergartenController(kid: ben)
ben.celebrateBirthday()
ben.celebrateBirthday()
ben.celebrateBirthday()
ben.celebrateBirthday()
ben.celebrateBirthday()
ben.celebrateBirthday()
ben.celebrateBirthday()
ben.celebrateBirthday()
ben.celebrateBirthday()
ben.celebrateBirthday()
ben.celebrateBirthday()
ben.celebrateBirthday()
ben.celebrateBirthday()

Và đây là những gì console print ra:

Benji thêm một tuổi: 6
Benji thêm một tuổi: 7
Benji thêm một tuổi: 8
Benji thêm một tuổi: 9
Benji thêm một tuổi: 10
Benji thêm một tuổi: 11
Benji thêm một tuổi: 12
Benji thêm một tuổi: 13
Benji thêm một tuổi: 14
Benji thêm một tuổi: 15
Benji thêm một tuổi: 16
Benji thêm một tuổi: 17
Benji thêm một tuổi: 18
Haha 18 rồi éo phải kid nữa đâu

Chắc có nhiều bạn băn khoăn: tại sao lại là dấu ” ” mà không phải các cái khác?
Đây là câu trả lời nhé:

During review many different sigils were considered:

  • No Sigil: This matches function type references, but suffers from ambiguity with wanting to actually call a type property. Having to type let foo: KeyPath<Baz, Bar> while consistent with function type references, really is not that great (even for function type references).

  • Back Tick: Borrowing from lisp, back-tick was what we used in initial discussions of this proposal (it was easy to write on a white-board), but it was not chosen because it is hard to type in markdown, and comes dangerously close to conflicting with other parser intrinsics.

  • Pound: We considered # as well, and while it is appealing, we’d like to save it for the future. # also has a slightly more computational connotation in Swift so far. For instance, #keyPath ‘identifies if its valid and returns a String’, #available does the neccesary computation to verify availability and yields a boolean.

  • Back Slash: Where # is computational, in Swift has more of a ‘behave differently for a moment’ connotation, and that seems to fit exactly what we want when forming a key path.

  • Function Type References
    We think the disambiguating benefits of the escape-sigil would greatly benefit function type references, but such considerations are outside the scope of this proposal.

Tài liệu tham khảo của bài viết: