KeyPath คือพาธแบบมีชนิดไปยังพร็อพเพอร์ตี้ของอ็อบเจกต์ใน Swift ซึ่งแสดงเป็นค่าชั้นหนึ่ง ต่างจากคีย์สตริงใน KVC (Key-Value Coding) KeyPath จะถูกตรวจสอบโดยคอมไพเลอร์ในเวลาคอมไพล์: คอมไพเลอร์รู้จักชนิดของอ็อบเจกต์ราก ชนิดของพร็อพเพอร์ตี้ และสามารถรับประกันว่าพาธมีอยู่ KeyPath ถูกใช้ใน Combine สำหรับการผูกแบบรีแอกทีฟ ใน SwiftUI สำหรับการผูกสองทาง และในไลบรารีมาตรฐานสำหรับการเรียงลำดับและกรองคอลเล็กชัน ตาม Apple Developer, 2025 KeyPath เป็นส่วนประกอบพื้นฐานสำหรับการเขียนโปรแกรมเชิงฟังก์ชันใน Swift
ประเด็นสำคัญ
KeyPath เป็นชนิดจากไลบรารีมาตรฐานของ Swift ที่แทนพาธไปยังพร็อพเพอร์ตี้ของอ็อบเจกต์ ต่างจากการเข้าถึงพร็อพเพอร์ตี้ด้วยตนเองผ่าน dot notation (object.property) หรือคีย์สตริง (value(forKey:)) KeyPath เป็นอ็อบเจกต์ชั้นหนึ่ง: สามารถส่งเป็นอาร์กิวเมนต์ เก็บในตัวแปร และประกอบรวมได้
KeyPath เขียนด้วยเครื่องหมายแบ็กสแลชตามด้วยพาธไปยังพร็อพเพอร์ตี้: \Person.name พาธสามารถซ้อนกันได้: \Person.address.city คอมไพเลอร์ตรวจสอบว่า Person มีพร็อพเพอร์ตี้ name และ name เป็น String จริงๆ หากพร็อพเพอร์ตี้ไม่มีอยู่หรือชนิดผิด โค้ดจะไม่คอมไพล์
struct Person: Codable {
let name: String
let age: Int
var isActive: Bool
}
// KeyPath (อ่านอย่างเดียว)
let namePath: KeyPath<Person, String> = \Person.name
let person = Person(name: "Alice", age: 30, isActive: true)
let name = person[keyPath: namePath]
การเข้าถึงพร็อพเพอร์ตี้ผ่าน KeyPath ทำได้ผ่านซับสคริปต์ object[keyPath: path] สำหรับพร็อพเพอร์ตี้แบบอ่านอย่างเดียว ใช้ KeyPath หากพร็อพเพอร์ตี้เปลี่ยนแปลงได้ (var) จะมี WritableKeyPath ให้ใช้ สำหรับชนิดอ้างอิง ReferenceWritableKeyPath อนุญาตให้แก้ไขพร็อพเพอร์ตี้โดยตรงผ่าน KeyPath
KeyPath มีลำดับชั้นชนิดย่อยที่สะท้อนระดับการเข้าถึงพร็อพเพอร์ตี้ ลำดับชั้นพื้นฐานคือ: AnyKeyPath → PartialKeyPath → KeyPath → WritableKeyPath → ReferenceWritableKeyPath แต่ละชนิดย่อยเพิ่มความสามารถ
KeyPath — อ่านอย่างเดียว WritableKeyPath — อ่านและเขียนสำหรับชนิดค่า (struct) ReferenceWritableKeyPath — อ่านและเขียนสำหรับชนิดอ้างอิง (class) Swift เลือกชนิดย่อยที่ถูกต้องโดยอัตโนมัติตามบริบท
| ชนิด | การเข้าถึง | ชนิดอ็อบเจกต์ | การเปลี่ยนแปลง |
|---|---|---|---|
| KeyPath | อ่านอย่างเดียว | ใดๆ | ไม่ |
| WritableKeyPath | อ่านและเขียน | ชนิดค่า (inout) | ผ่านบริบท mutating |
| ReferenceWritableKeyPath | อ่านและเขียน | ชนิดอ้างอิง | กำหนดค่าโดยตรง |
// WritableKeyPath สำหรับชนิดค่า
var mutablePerson = Person(name: "Bob", age: 25, isActive: false)
let writablePath: WritableKeyPath<Person, Bool> = \Person.isActive
mutablePerson[keyPath: writablePath] = true
// ReferenceWritableKeyPath สำหรับคลาส
class User: NSObject {
@objc dynamic var name: String = ""
}
let user = User()
let refPath: ReferenceWritableKeyPath<User, String> = \User.name
user[keyPath: refPath] = "Charlie"
ข้อแตกต่างสำคัญ: สำหรับชนิดค่า การตั้งค่าผ่าน KeyPath ต้องการ var (บริบท inout) ในขณะที่ชนิดอ้างอิงต้องการแค่พร็อพเพอร์ตี้ที่เปลี่ยนแปลงได้ ซึ่งสอดคล้องกับความหมายทั่วไปของ Swift: ชนิดค่าถูกส่งโดยค่า ดังนั้นการเปลี่ยนแปลงจึงต้องการความสามารถในการเปลี่ยนแปลงของคอนเทนเนอร์
KeyPath มีบทบาทสำคัญใน Combine และ SwiftUI ใน Combine ตัวดำเนินการ assign ใช้ KeyPath เพื่อผูกค่าของผู้เผยแพร่กับพร็อพเพอร์ตี้ของอ็อบเจกต์ ใน SwiftUI Binding ใช้ KeyPath สำหรับการเชื่อมโยงสองทางระหว่างโมเดลและมุมมอง
ตัวดำเนินการ assign(to:on:) รับ ReferenceWritableKeyPath และอ็อบเจกต์ เมื่อผู้เผยแพร่ส่งค่า มันจะถูกเขียนไปยังพร็อพเพอร์ตี้ที่ระบุโดยอัตโนมัติ นี่เป็นวิธีประกาศเพื่อจัดการสถานะโดยไม่ต้องกำหนดค่าด้วยตนเอง
import Combine
class SettingsViewModel: ObservableObject {
@Published var volume: Float = 0.5
var cancellables = Set<AnyCancellable>()
func bindSlider(publisher: AnyPublisher<Float, Never>) {
publisher
.assign(to: \SettingsViewModel.volume, on: self)
.store(in: &cancellables)
}
}
ใน SwiftUI @Binding ใช้ KeyPath เพื่อเชื่อมต่อมุมมองแม่และลูก แม่ส่ง Binding
KeyPath ช่วยให้เขียนฟังก์ชันการเรียงลำดับและกรองที่ยืดหยุ่นและปลอดภัยด้านชนิด แทนที่จะส่งคลosureทุกครั้ง คุณสามารถส่ง KeyPath ไปยังพร็อพเพอร์ตี้ที่คุณต้องการเรียงลำดับคอลเล็กชัน ทำให้โค้ดสะอาดขึ้นและลดการทำซ้ำ
ฟังก์ชัน sorted
extension Sequence {
func sorted<Value: Comparable>(
by keyPath: KeyPath<Element, Value>,
ascending: Bool = true
) -> [Element] {
ascending
? self.sorted { $0[keyPath: keyPath] < $1[keyPath: keyPath] }
: self.sorted { $0[keyPath: keyPath] > $1[keyPath: keyPath] }
}
}
let sortedByName = people.sorted(by: \Person.name)
let sortedByAgeDesc = people.sorted(by: \Person.age, ascending: false)
การนำเสนอนี้เรียงลำดับตามพร็อพเพอร์ตี้ Comparable ใดๆ โดยไม่ต้องเขียนคลosure ในการเรียงลำดับตาม นามสกุล เพียงเปลี่ยน KeyPath: \Person.lastName หากต้องการเรียงลำดับตามคีย์ที่กำหนดเอง (เช่น ความยาวชื่อ) จะใช้ map ภายใน — นักพัฒนายังสามารถเขียนคลosure ของตนเองสำหรับกรณีที่ไม่ได้มาตรฐาน
AnyKeyPath เป็นชนิดพื้นฐานในลำดับชั้น KeyPath ที่ลบข้อมูลเกี่ยวกับชนิดรากและค่าเฉพาะ AnyKeyPath ไม่รู้ว่าถูกเรียกบนชนิดอ็อบเจกต์ใดหรือส่งคืนชนิดค่าใด ซึ่งมีประโยชน์สำหรับการเก็บ KeyPaths ต่างชนิดในคอลเล็กชันและสำหรับการสะท้อน
AnyKeyPath ใช้เมื่อคุณต้องการเก็บรายการพาธไปยังพร็อพเพอร์ตี้ต่างๆ ของชนิดต่างๆ ตัวอย่างเช่น ใน ตัวกำหนดค่า UI ที่แต่ละพาธกำหนดค่าพร็อพเพอร์ตี้เฉพาะ และชนิดของพร็อพเพอร์ตี้อาจแตกต่างกัน AnyKeyPath ช่วยให้ทำงานกับทุกพาธได้อย่างเป็นเอกภาพ
protocol Configurable {
func apply(_ keyPath: any PartialKeyPath<Self>, value: any)
}
extension Configurable {
func configure(_ pairs: (any KeyPath<Self, any>, any)...) {
for (path, value) in pairs {
apply(path, value: value)
}
}
}
extension Person: Configurable { }
// อนุญาตให้กำหนดค่าพร็อพเพอร์ตี้ผ่าน AnyKeyPath
AnyKeyPath ต้องการ การแปลงชนิด เมื่อดึงค่า เนื่องจากชนิดเฉพาะถูกลบไปแล้ว เพื่อความปลอดภัยด้านชนิดอย่างสมบูรณ์ ควรใช้ KeyPath
KeyPath มีประสิทธิภาพมากกว่าคีย์สตริง KVC (value(forKey:)) อย่างมาก คีย์สตริงถูกแก้ไขผ่านกลไก runtime ของ Objective-C ซึ่งรวมถึงการแยกวิเคราะห์สตริง การค้นหาใน runtime และการแปลงชนิด KeyPath เป็นกลไกแบบสถิตที่อิงตามโครงสร้างดั้งเดิมของ Swift
ตามเกณฑ์มาตรฐาน KeyPath โดยเฉลี่ย เร็วกว่า 10-20 เท่า เมื่อเทียบกับ value(forKey:) สำหรับการเข้าถึงพร็อพเพอร์ตี้ ความแตกต่างเกิดจากการไม่มีการค้นหาใน runtime และการจัดส่งแบบไดนามิก KeyPath เข้าถึง offset ของพร็อพเพอร์ตี้ในหน่วยความจำโดยตรงด้วยการเพิ่มประสิทธิภาพคอมไพเลอร์ที่เพียงพอ
func readWithKeyPath(person: Person) -> String {
person[keyPath: \Person.name]
}
func readWithKVC(person: Person) -> String {
person.value(forKey: "name") as! String
}
KVC ไม่เพียงช้ากว่า แต่ยัง ไม่ปลอดภัยด้านชนิด อีกด้วย: สตริง “name” อาจไม่มีอยู่และการแปลง as! String อาจล้มเหลว KeyPath รับประกันการมีอยู่ของพร็อพเพอร์ตี้และความถูกต้องของชนิดในเวลาคอมไพล์ สำหรับส่วนที่สำคัญต่อประสิทธิภาพ (แอนิเมชัน รายการที่มีรายการนับพัน) KeyPath เป็นทางเลือกที่ถูกต้องเพียงทางเดียว
คำถามที่พบบ่อย
KeyPath คือตัวชี้ไปยังพร็อพเพอร์ตี้ของอ็อบเจกต์ที่สามารถส่งเป็นค่าปกติได้ แทนที่จะเขียน person.name คุณสร้างพาธ \Person.name และใช้มันเพื่ออ่านหรือเขียนพร็อพเพอร์ตี้บนอินสแตนซ์ Person ใดๆ
KeyPath — อ่านอย่างเดียว WritableKeyPath — สำหรับอ่านและเขียนชนิดค่า (struct) ผ่านบริบท inout ReferenceWritableKeyPath — สำหรับอ่านและเขียนชนิดอ้างอิง (class) โดยไม่มีเงื่อนไขเพิ่มเติม
KeyPath ถูกใช้อย่างแพร่หลายใน Combine (assign(to:on:)), SwiftUI (Binding, FocusState), การเรียงลำดับคอลเล็กชัน (sorted(by:)), Core Data (NSSortDescriptor) และสำหรับการเข้าถึงพร็อพเพอร์ตี้ที่ซ้อนกันอย่างปลอดภัย
ใช่ KeyPath ปลอดภัยด้านชนิดอย่างสมบูรณ์ ข้อผิดพลาดในพาธ (พิมพ์ผิด ชนิดผิด) ถูกตรวจพบในเวลาคอมไพล์ ไม่ใช่ในรันไทม์ value(forKey:) ใช้สตริงและอาจล้มเหลวด้วย NSUndefinedKeyException หากคีย์ไม่มีอยู่
ไม่โดยตรง — Swift ไม่สนับสนุนการสร้าง KeyPath แบบไดนามิกจากสตริง อย่างไรก็ตาม คุณสามารถใช้ AnyKeyPath เพื่อเก็บและส่ง KeyPaths ที่ได้รับแบบสแตติก สำหรับการเข้าถึงพร็อพเพอร์ตี้แบบไดนามิกโดยสตริง ให้ใช้ value(forKey:) กับ runtime ของ Objective-C
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ