Кажется, что можно использовать массив KeyPath
в качестве ключей сортировки для сортировки массива структур Swift с использованием произвольного количества ключей сортировки.
Концептуально все просто. Вы определяете массив KeyPaths для универсального объекта, где единственным ограничением является то, что свойства в keypath должны быть Comparable
.
Пока все KeyPaths указывают на свойства одного типа, все в порядке. Однако как только вы попытаетесь использовать KeyPaths, которые указывают на элементы разных типов в массиве, он перестает работать.
См. Код ниже. Я создаю простую структуру с двумя свойствами Int и двойным свойством. Я создаю расширение для Array, которое реализует функцию sortedByKeypaths(_:)
Эта функция определяет универсальный тип PROPERTY, который является Comparable. Требуется массив путей к некоторому объекту Element, который задает свойства типа PROPERTY. (Сопоставимые свойства.)
Пока вы вызываете эту функцию, используя массив KeyPaths к свойствам одного типа, она работает отлично.
Однако, если вы попытаетесь передать массив путей ключей к свойствам разных типов, это вызовет ошибку. "невозможно преобразовать значение типа" [PartialKeyPath] "в ожидаемый тип аргумента" [KeyPath] "»
Поскольку массив содержит разнородные пути ключей, массив переходит к типу «[PartialKeyPath]» из-за стирания типа, и вы не можете использовать PartialKeyPath для извлечения элементов из массива.
Есть ли решение этой проблемы? Невозможность использовать разнородный массив KeyPath, похоже, сильно ограничивает полезность Swift KeyPaths.
import UIKit
struct Stuff {
let value: Int
let value2: Int
let doubleValue: Double
}
extension Array {
func sortedByKeypaths<PROPERTY: Comparable>(_ keypaths: [KeyPath<Element, PROPERTY>]) -> [Element] {
return self.sorted { lhs, rhs in
var keypaths = keypaths
while !keypaths.isEmpty {
let keypath = keypaths.removeFirst()
if lhs[keyPath: keypath] != rhs[keyPath: keypath] {
return lhs[keyPath: keypath] < rhs[keyPath: keypath]
}
}
return true
}
}
}
var stuff = [Stuff]()
for _ in 1...20 {
stuff.append(Stuff(value: Int(arc4random_uniform(5)),
value2: Int(arc4random_uniform(5)),
doubleValue: Double(arc4random_uniform(10))))
}
let sortedStuff = stuff.sortedByKeypaths([\Stuff.value, \Stuff.value2]) //This works
sortedStuff.forEach { print($0) }
let moreSortedStuff = stuff.sortedByKeypaths([\Stuff.value, \Stuff.doubleValue]) //This throws a compiler error
moreSortedStuff.forEach { print($0) }
Проблема с использованием массива частичных путей к ключам заключается в том, что у вас нет гарантии, что типом свойств является Comparable
. Одним из возможных решений было бы использовать обертку стирания типа, чтобы стереть тип значения ключевого пути, при этом убедившись, что это Comparable
:
struct PartialComparableKeyPath<Root> {
private let _isEqual: (Root, Root) -> Bool
private let _isLessThan: (Root, Root) -> Bool
init<Value : Comparable>(_ keyPath: KeyPath<Root, Value>) {
self._isEqual = { $0[keyPath: keyPath] == $1[keyPath: keyPath] }
self._isLessThan = { $0[keyPath: keyPath] < $1[keyPath: keyPath] }
}
func isEqual(_ lhs: Root, _ rhs: Root) -> Bool {
return _isEqual(lhs, rhs)
}
func isLessThan(_ lhs: Root, _ rhs: Root) -> Bool {
return _isLessThan(lhs, rhs)
}
}
Затем вы можете реализовать свою функцию сортировки как:
extension Sequence {
func sorted(by keyPaths: PartialComparableKeyPath<Element>...) -> [Element] {
return sorted { lhs, rhs in
for keyPath in keyPaths {
if !keyPath.isEqual(lhs, rhs) {
return keyPath.isLessThan(lhs, rhs)
}
}
return false
}
}
}
а затем используйте вот так:
struct Stuff {
let value: Int
let value2: Int
let doubleValue: Double
}
var stuff = [Stuff]()
for _ in 1 ... 20 {
stuff.append(Stuff(value: Int(arc4random_uniform(5)),
value2: Int(arc4random_uniform(5)),
doubleValue: Double(arc4random_uniform(10))))
}
let sortedStuff = stuff.sorted(by: PartialComparableKeyPath(\.value),
PartialComparableKeyPath(\.value2))
sortedStuff.forEach { print($0) }
let moreSortedStuff = stuff.sorted(by: PartialComparableKeyPath(\.value),
PartialComparableKeyPath(\.doubleValue))
moreSortedStuff.forEach { print($0) }
Хотя, к сожалению, для этого требуется обернуть каждый отдельный путь ключа значением PartialComparableKeyPath
, чтобы захватить и стереть тип значения для пути ключа, что не особенно красиво.
На самом деле нам здесь нужна функция вариативные генерики, которая позволит вам определять вашу функцию с помощью переменного числа общих заполнителей для типов значений ваших ключевых путей, каждый из которых ограничен до Comparable
.
До тех пор другой вариант - просто записать заданное количество перегрузок для разного количества путей для сравнения:
extension Sequence {
func sorted<A : Comparable>(by keyPathA: KeyPath<Element, A>) -> [Element] {
return sorted { lhs, rhs in
lhs[keyPath: keyPathA] < rhs[keyPath: keyPathA]
}
}
func sorted<A : Comparable, B : Comparable>
(by keyPathA: KeyPath<Element, A>, _ keyPathB: KeyPath<Element, B>) -> [Element] {
return sorted { lhs, rhs in
(lhs[keyPath: keyPathA], lhs[keyPath: keyPathB]) <
(rhs[keyPath: keyPathA], rhs[keyPath: keyPathB])
}
}
func sorted<A : Comparable, B : Comparable, C : Comparable>
(by keyPathA: KeyPath<Element, A>, _ keyPathB: KeyPath<Element, B>, _ keyPathC: KeyPath<Element, C>) -> [Element] {
return sorted { lhs, rhs in
(lhs[keyPath: keyPathA], lhs[keyPath: keyPathB], lhs[keyPath: keyPathC]) <
(rhs[keyPath: keyPathA], rhs[keyPath: keyPathB], rhs[keyPath: keyPathC])
}
}
func sorted<A : Comparable, B : Comparable, C : Comparable, D : Comparable>
(by keyPathA: KeyPath<Element, A>, _ keyPathB: KeyPath<Element, B>, _ keyPathC: KeyPath<Element, C>, _ keyPathD: KeyPath<Element, D>) -> [Element] {
return sorted { lhs, rhs in
(lhs[keyPath: keyPathA], lhs[keyPath: keyPathB], lhs[keyPath: keyPathC], lhs[keyPath: keyPathD]) <
(rhs[keyPath: keyPathA], rhs[keyPath: keyPathB], rhs[keyPath: keyPathC], rhs[keyPath: keyPathD])
}
}
func sorted<A : Comparable, B : Comparable, C : Comparable, D : Comparable, E : Comparable>
(by keyPathA: KeyPath<Element, A>, _ keyPathB: KeyPath<Element, B>, _ keyPathC: KeyPath<Element, C>, _ keyPathD: KeyPath<Element, D>, _ keyPathE: KeyPath<Element, E>) -> [Element] {
return sorted { lhs, rhs in
(lhs[keyPath: keyPathA], lhs[keyPath: keyPathB], lhs[keyPath: keyPathC], lhs[keyPath: keyPathD], lhs[keyPath: keyPathE]) <
(rhs[keyPath: keyPathA], rhs[keyPath: keyPathB], rhs[keyPath: keyPathC], rhs[keyPath: keyPathD], rhs[keyPath: keyPathE])
}
}
func sorted<A : Comparable, B : Comparable, C : Comparable, D : Comparable, E : Comparable, F : Comparable>
(by keyPathA: KeyPath<Element, A>, _ keyPathB: KeyPath<Element, B>, _ keyPathC: KeyPath<Element, C>, _ keyPathD: KeyPath<Element, D>, _ keyPathE: KeyPath<Element, E>, _ keyPathF: KeyPath<Element, F>) -> [Element] {
return sorted { lhs, rhs in
(lhs[keyPath: keyPathA], lhs[keyPath: keyPathB], lhs[keyPath: keyPathC], lhs[keyPath: keyPathD], lhs[keyPath: keyPathE], lhs[keyPath: keyPathF]) <
(rhs[keyPath: keyPathA], rhs[keyPath: keyPathB], rhs[keyPath: keyPathC], rhs[keyPath: keyPathD], rhs[keyPath: keyPathE], rhs[keyPath: keyPathF])
}
}
}
Я определил им до 6 путей, которых должно хватить для большинства случаев сортировки. Здесь мы используем перегруженное сравнение лексикографических кортежей <
, что также продемонстрировало здесь.
Хотя реализация не очень хороша, сайт вызова теперь выглядит намного лучше, поскольку он позволяет вам сказать:
let sortedStuff = stuff.sorted(by: \.value, \.value2)
sortedStuff.forEach { print($0) }
let moreSortedStuff = stuff.sorted(by: \.value, \.doubleValue)
moreSortedStuff.forEach { print($0) }
Хотя мне нравится ваша обертка для стирания текста. Придется с этим поэкспериментировать. (Проголосовал)
И снова отличный ответ! - Меня просто интересуют частные подчеркнутые свойства в struct PartialComparableKeyPath. Почему бы вам просто не определить два (общедоступных) свойства let isEqual: (Root, Root) -> Bool
и let isLessThan: (Root, Root) -> Bool
вместо методов экземпляра?
@MartinR Спасибо! Я обычно считаю, что методы более эргономичны, чем свойства, типизированные для функций, поскольку они позволяют использовать метки аргументов (хотя на самом деле не используются в этом конкретном случае), и они лучше автозаполняются - со свойством типа функции Swift просто автоматически заполняет базовое имя , тогда как с методами он также автоматически заполняет заполнители для аргументов, что приятно. Кроме того, я не думаю, что есть какая-то техническая причина отдавать предпочтение одному в данном случае.
Я думал о переменном количестве аргументов. Сначала я попытался создать 1 функцию с максимальным количеством необязательных аргументов KeyPath и значением по умолчанию nil для всех, кроме первого, но общий Comparable предотвращает это. Кажется неправильным иметь бесчисленное количество разных версий метода с переменным количеством аргументов (и нарушает принцип DRY).