Как использовать гетерогенный массив Swift KeyPaths

Кажется, что можно использовать массив 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) }
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
7
0
1 829
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

Я думал о переменном количестве аргументов. Сначала я попытался создать 1 функцию с максимальным количеством необязательных аргументов KeyPath и значением по умолчанию nil для всех, кроме первого, но общий Comparable предотвращает это. Кажется неправильным иметь бесчисленное количество разных версий метода с переменным количеством аргументов (и нарушает принцип DRY).

Duncan C 02.05.2018 18:36

Хотя мне нравится ваша обертка для стирания текста. Придется с этим поэкспериментировать. (Проголосовал)

Duncan C 02.05.2018 18:38

И снова отличный ответ! - Меня просто интересуют частные подчеркнутые свойства в struct PartialComparableKeyPath. Почему бы вам просто не определить два (общедоступных) свойства let isEqual: (Root, Root) -> Bool и let isLessThan: (Root, Root) -> Bool вместо методов экземпляра?

Martin R 03.05.2018 15:38

@MartinR Спасибо! Я обычно считаю, что методы более эргономичны, чем свойства, типизированные для функций, поскольку они позволяют использовать метки аргументов (хотя на самом деле не используются в этом конкретном случае), и они лучше автозаполняются - со свойством типа функции Swift просто автоматически заполняет базовое имя , тогда как с методами он также автоматически заполняет заполнители для аргументов, что приятно. Кроме того, я не думаю, что есть какая-то техническая причина отдавать предпочтение одному в данном случае.

Hamish 03.05.2018 15:51

Другие вопросы по теме