Một KeyPath tốn những gì
\User.name trông như cú pháp — một thứ ở thời điểm biên dịch, kiểu như một tham chiếu hàm. Nó là
một đối tượng cấp phát trên heap với cả một cây phân cấp class, và biết điều đó thì giải thích được
cả khi nào key path miễn phí lẫn khi nào thì không.
Cây phân cấp
AnyKeyPath
└── PartialKeyPath<Root>
└── KeyPath<Root, Value>
└── WritableKeyPath<Root, Value>
└── ReferenceWritableKeyPath<Root, Value>
Mỗi tầng thêm một năng lực. KeyPath đọc. WritableKeyPath ghi vào một var trên một kiểu giá trị.
ReferenceWritableKeyPath ghi xuyên qua một tham chiếu let tới một class — và đó là lý do
assign(to:on:) trong Combine nhận đúng cái kiểu đó.
struct User { var name: String }
final class Session { var token: String? }
let read: KeyPath<User, String> = \.name
let write: WritableKeyPath<User, String> = \.name
let refWrite: ReferenceWritableKeyPath<Session, String?> = \.token
Khi nào chúng miễn phí
Phần lớn thời gian. Trình biên dịch giỏi việc này.
users.map(\.name)
users.sorted(using: KeyPathComparator(\.age))
users.filter { $0.isActive }
map(\.name) với một key path viết thẳng tại chỗ sẽ được tối ưu thành một phép truy cập thuộc tính
trực tiếp trong bản release. Không cấp phát và không gián tiếp — cái key path chưa bao giờ thật sự
tồn tại lúc chạy.
Đó là lý do cách viết này ổn ở khắp nơi và tôi sẽ không nghĩ ngợi gì về map(\.name) trong một vòng
lặp.
Khi nào chúng không miễn phí
Phép tối ưu đó phụ thuộc vào việc key path là một hằng viết thẳng, đã biết tại chỗ gọi. Phá vỡ một trong hai điều kiện và bạn nhận về cái đối tượng.
Lưu trong một thuộc tính hay một mảng:
struct Column {
let title: String
let value: KeyPath<User, String> // một đối tượng thật, được cấp phát
}
let columns = [
Column(title: "Tên", value: \.name),
Column(title: "Email", value: \.email),
]
Giờ mỗi key path là một đối tượng trên heap, và đọc qua một cái là một lượt tra cứu động — nó duyệt một vùng đệm các độ dời thay vì biên dịch thành một độ dời cố định.
Bị xóa kiểu thành AnyKeyPath: mất hết thông tin kiểu và mọi cơ hội chuyên biệt hóa.
Bị băm hay so sánh: AnyKeyPath tuân thủ Hashable, thứ rất tiện cho một dictionary khóa theo
thuộc tính, và việc băm sẽ duyệt qua các thành phần của key path.
Mẹo
Quy tắc bao phủ tất cả: một key path viết thẳng truyền trực tiếp vào một hàm thì miễn phí; một key path lưu trong một biến thì là một đối tượng. Nếu bạn đang dựng một bảng các cột, một định nghĩa biểu mẫu, hay một cấu hình ánh xạ thì bạn đang ở trường hợp thứ hai — và như thế cũng ổn, vì những thứ đó được dựng một lần rồi đọc ở tốc độ giao diện.
Chỗ dạng đối tượng là đáng giá
Trường hợp động không phải thứ để né tránh — nó mở ra những thiết kế mà nếu không có thì bất khả thi.
Cấu hình generic, an toàn kiểu:
struct Column<Root> {
let title: String
let value: (Root) -> String
}
extension Column {
init<V: CustomStringConvertible>(_ title: String, _ keyPath: KeyPath<Root, V>) {
self.title = title
self.value = { String(describing: $0[keyPath: keyPath]) }
}
}
let columns = [
Column("Tên", \User.name),
Column("Tuổi", \User.age),
]
An toàn kiểu, an toàn khi đổi tên, và bất khả thi nếu dùng chuỗi. Đổi tên name sẽ là một lỗi biên
dịch chứ không phải một bất ngờ lúc chạy.
@dynamicMemberLookup cho các kiểu bọc:
@dynamicMemberLookup
struct Validated<Value> {
let value: Value
subscript<T>(dynamicMember keyPath: KeyPath<Value, T>) -> T {
value[keyPath: keyPath]
}
}
let user = Validated(value: User(name: "Tuấn", age: 30))
user.name // đọc xuyên qua mà không cần bóc lớp bọc
Đây chính là cách @Bindable trong SwiftUI tạo ra $model.property — tra cứu thành viên động bằng
key path.
Phép đo
Với bất cứ thứ gì làm việc thật, chuyện này là nhiễu. Đọc một key path đã lưu tốn nhiều hơn một phép truy cập thuộc tính trực tiếp đại khái vài nano giây — một lượt duyệt vùng đệm thay vì một độ dời cố định.
Điều đó quan trọng trong một vòng lặp trên một triệu phần tử. Nó không quan trọng trong một bảng, một biểu mẫu, một bộ so sánh khi sắp xếp, hay bất cứ đâu có động đến màn hình hay mạng.
Trường hợp duy nhất tôi đo được là đáng kể: một lượt sắp xếp trên một mảng lớn dùng một
KeyPathComparator đã lưu trong một vòng lặp chặt. Chuyển sang một bộ so sánh dạng closure thì nhanh
hơn một cách đo được, và nó cũng khó đọc hơn, và tôi đã giữ lại bản dùng key path.
Rút ra điều gì
Hãy dùng thoải mái các key path viết thẳng — map(\.name), sorted(using:), assign(to:on:).
Chúng miễn phí và đọc hay hơn closure.
Hãy lưu chúng khi thiết kế đòi hỏi — định nghĩa cột, các kiểu bọc generic, tra cứu thành viên động. Cái giá của đối tượng là có thật và nó được trả ở lúc cấu hình, không phải trong một vòng lặp.
Chỉ với tới một closure thay cho key path nếu một trình đo hiệu năng đã bảo bạn làm thế, và cho tới nay nó mới bảo tôi đúng một lần.