如何在Swift中使用异构KeyPath数组实现多键排序?
嘿,关于用KeyPath数组给Swift结构体数组做任意多键排序这件事,我琢磨过不少——概念上确实挺直观的:你只需要定义一个指向泛型对象的KeyPath数组,唯一的硬性要求就是这些KeyPath指向的属性得遵循Comparable协议。
一切正常的场景:同类型属性的多键排序
当所有KeyPath指向相同类型的属性时,这套逻辑完全顺畅。举个实际的例子,假设我们有个User结构体:
struct User { let name: String let age: Int let score: Int }
如果我们想先按age排序,年龄相同再按score排序,就可以创建一个[KeyPath<User, Int>]类型的数组,然后利用它实现排序:
let sampleUsers: [User] = [ User(name: "Alice", age: 25, score: 85), User(name: "Bob", age: 22, score: 90), User(name: "Charlie", age: 25, score: 95) ] let sortKeys: [KeyPath<User, Int>] = [\.age, \.score] let sortedUsers = sampleUsers.sorted { lhs, rhs in // 遍历排序键,找到第一个能区分元素的属性 for keyPath in sortKeys { let lhsValue = lhs[keyPath: keyPath] let rhsValue = rhs[keyPath: keyPath] if lhsValue != rhsValue { return lhsValue < rhsValue } } // 所有排序键都相等,返回false(保持原顺序) return false }
这段代码运行起来完全没问题,结果会先按年龄从小到大排,同年龄的再按分数从低到高排。
踩坑点:不同类型属性的KeyPath无法共存
但一旦你尝试把指向不同类型属性的KeyPath塞进同一个数组,立刻就会遇到类型不兼容的问题。比如你想把name: String(同样遵循Comparable)也加入排序逻辑,先按name排,再按age排——这时候你会发现根本没法创建包含\.name和\.age的数组:KeyPath<User, String>和KeyPath<User, Int>是完全不同的类型,Swift的强类型数组要求所有元素类型一致,自然没法把它们混在一起。
本质上是因为Swift的KeyPath类型和它指向的属性类型强绑定,不同属性类型对应的KeyPath没有统一的公共父类型(除了Any,但这样会丢失类型信息,没法在排序时做比较)。如果非要实现跨类型的多键排序,就得做类型擦除,比如封装一个能处理任意Comparable类型的包装器,但这会大幅增加代码复杂度,远不如同类型KeyPath数组的方案简洁。
内容的提问来源于stack exchange,提问作者Duncan C

