Как использовать гетерогенный массив Swift KeyPaths - PullRequest
0 голосов
/ 02 мая 2018

Похоже, можно использовать массив KeyPath s в качестве ключей сортировки для сортировки массива структур Swift с использованием произвольного числа ключей сортировки. Концептуально все просто. Вы определяете массив KeyPaths для универсального объекта, где единственным ограничением является то, что свойства в keypath будут Comparable.

Пока все KeyPaths указывают на свойства одного типа, все в порядке. Однако, как только вы попытаетесь использовать KeyPath, указывающие на элементы разных типов в массиве, он перестает работать.

См. Код ниже. Я создаю простую структуру с 2 свойствами Int и двойным свойством. Я создаю расширение для Array, которое реализует функцию sortedByKeypaths(_:) Эта функция указывает универсальный тип PROPERTY, который является сопоставимым. Это берет массив kepaths к некоторому объекту Element, который определяет свойства типа PROPERTY. (Сопоставимые свойства.)

Пока вы вызываете эту функцию, используя массив KeyPaths для свойств одного типа, она отлично работает.

Однако, если вы попытаетесь передать массив путей к свойствам разных типов, это вызовет ошибку "невозможно преобразовать значение типа '[PartialKeyPath]' в ожидаемый тип аргумента '[KeyPath]'"

Поскольку массив содержит разнородные пути к ключам, массив передается типу '[PartialKeyPath] `из-за стирания типа, и вы не можете использовать PartialKeyPath для извлечения элементов из массива.

Есть ли решение этой проблемы? Невозможность использования разнородного массива KeyPaths, похоже, сильно ограничивает полезность 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) }

1 Ответ

0 голосов
/ 02 мая 2018

Проблема с использованием массива частичных путей к ключам состоит в том, что вы не можете гарантировать, что типы свойств 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) }
...