Чтобы отсортировать локальный массив Swift по нескольким полям, сравнивайте каждый ключ в порядке приоритета и переходите к следующему, только если значения равны. Отдельно определите резервные значения для необязательных ключей, а затем решите, где должны располагаться отсутствующие значения. В этом примере синтетические метки фигур упорядочиваются для списка на рабочем столе Mac.

Сначала определите правила сортировки

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

Для двух решений нужны отдельные правила. Резервный ключ выбирает значение для одного элемента: используйте его предпочтительную позицию, если она задана, иначе — резервную. Дополнительный критерий сравнения определяет другое поле, только если эффективные позиции двух элементов равны. Оператор Swift для подстановки значения nil, ??, помогает принять первое решение: он выбирает обёрнутое значение или резервное. Базовые операторы — оператор подстановки nil

В этом примере используются следующие явные правила:

Приоритет Ключ Правило
1 Группа Сначала меньшие числа
2 Эффективная позиция preferredPosition ?? backupPosition
3 Размещение отсутствующей позиции В каждой группе элементы без позиции идут после элементов с позицией
4 Заголовок Возрастание по стандартному строковому сравнению Swift при равных позициях
5 Идентификатор Сначала меньшие идентификаторы, если все предыдущие ключи равны

Размещение элементов без позиции — часть сравнения позиций, а не отдельное поле, которое переопределяет группу. Элемент из группы ноль всё равно идёт перед элементом из группы один, даже если у первого элемента нет позиции.

В этих синтетических данных для сравнения заголовков намеренно используется стандартный строковый порядок Swift. Сохраняйте этот выбор, если позже замените замыкание компараторами Foundation.

Выполните полную сортировку по нескольким полям с явной обработкой nil

Следующая полноценная программа использует стандартную библиотеку Swift и не требует импортов. Запустите её в среде с поддержкой Swift на Mac. Все входные данные заданы в программе; вычисление не использует внешние входные данные или постоянное хранилище. Операция сортировки возвращает новый массив с помощью sorted(by:). Документация Apple по sorted(by:)

struct ShapeItem {
    let id: Int
    let group: Int
    let preferredPosition: Int?
    let backupPosition: Int?
    let title: String

    var effectivePosition: Int? {
        preferredPosition ?? backupPosition
    }
}

func comesBefore(_ lhs: ShapeItem, _ rhs: ShapeItem) -> Bool {
    if lhs.group != rhs.group {
        return lhs.group < rhs.group
    }

    switch (lhs.effectivePosition, rhs.effectivePosition) {
    case let (.some(left), .some(right)):
        if left != right {
            return left < right
        }
    case (.some(_), .none):
        return true
    case (.none, .some(_)):
        return false
    case (.none, .none):
        break
    }

    if lhs.title != rhs.title {
        return lhs.title < rhs.title
    }

    return lhs.id < rhs.id
}

let items = [
    ShapeItem(id: 10, group: 1, preferredPosition: nil,
              backupPosition: 2, title: "Circle"),
    ShapeItem(id: 20, group: 1, preferredPosition: 1,
              backupPosition: 9, title: "Square"),
    ShapeItem(id: 30, group: 1, preferredPosition: nil,
              backupPosition: nil, title: "Arc"),
    ShapeItem(id: 40, group: 1, preferredPosition: 2,
              backupPosition: 0, title: "Arc"),
    ShapeItem(id: 50, group: 0, preferredPosition: nil,
              backupPosition: nil, title: "Dot"),
    ShapeItem(id: 60, group: 1, preferredPosition: 2,
              backupPosition: nil, title: "Arc"),
    ShapeItem(id: 70, group: 1, preferredPosition: 0,
              backupPosition: 8, title: "Zero")
]

let ordered = items.sorted(by: comesBefore)
let expectedIDs = [50, 70, 20, 40, 60, 10, 30]

precondition(ordered.map(\.id) == expectedIDs)
precondition(ordered.count == items.count)
precondition(items.reversed().sorted(by: comesBefore).map(\.id)
             == expectedIDs)

print(ordered.map(\.id))

Ожидаемый вывод:

[50, 70, 20, 40, 60, 10, 30]

Программу и ожидаемый порядок проверили, но здесь не запускали, поскольку в исследовательской среде нет компилятора Swift. Встроенные предварительные условия позволят проверить результат при локальном запуске.

Почему каждый пример занимает эту позицию

Элемент 50 идёт первым, потому что группа ноль имеет приоритет перед позицией. Затем элемент 70 показывает, что ноль — допустимая предпочтительная позиция: его резервное значение восемь не используется. Для элемента 20 используется предпочтительная позиция один.

У элементов 40, 60 и 10 эффективная позиция равна двум. Два заголовка Arc идут перед Circle, а идентификаторы разрешают оставшуюся ничью между 40 и 60. Элемент 30 идёт последним, поскольку в группе один отсутствуют оба поля позиции.

Это также отвечает на распространённый вопрос о резервном значении: для элемента 40 не используется резервная позиция ноль, когда его предпочтительная позиция совпадает с позицией другого элемента. По этим правилам резервное значение заменяет только отсутствующее предпочтительное. Если резервное значение должно также разрешать равенства, потребуется добавить отдельное правило сравнения.

Сохраняйте корректность компаратора при равных значениях

Swift требует, чтобы предикат сортировки задавал строгое слабое упорядочение. На практике это означает, что элемент никогда не должен идти перед самим собой, решения об упорядочении должны оставаться согласованными в разных сравнениях, а эквивалентные элементы должны образовывать согласованные группы. Apple документирует эти требования для sorted(by:). Требования к предикату

В примере перед возвратом результата сравнения проверяется, различается ли значение более приоритетного ключа. Заголовок сравнивается, только если совпадают группа и эффективная позиция; к идентификатору переходят, только если совпадают и заголовки. Если идентификаторы и все предыдущие ключи равны, последнее строгое сравнение возвращает false.

Не объединяйте сравнения независимых полей оператором ||. У одного элемента группа может быть выше по приоритету, а позиция — ниже; у другого сочетание может быть обратным. Тогда предикат с OR может утверждать, что каждый элемент должен идти перед другим. Последовательная обработка равенств исключает такое противоречие.

Также не используйте <= в качестве последнего сравнения: для равных значений оно вернёт true, нарушив правило, согласно которому элемент не может идти перед самим собой. Каждое сравнение должно зависеть только от переданных значений и фиксированных правил сортировки.

Выберите между сохранением порядка равных элементов и конечным идентификатором

Согласно документации, сортировка Swift стабильна: элементы, которые предикат считает эквивалентными, сохраняют взаимный порядок во входных данных. Если требуется именно такое поведение, уберите сравнение конечного идентификатора и возвращайте false после равенства заголовков. Гарантия стабильной сортировки

В этом примере оставшиеся равенства разрешаются с помощью уникальных синтетических идентификаторов. Поэтому разворот входных данных всё равно даёт ту же последовательность идентификаторов. Это свойство следует из правил и значений примера. Если идентификаторы и все остальные ключи повторяются, такие элементы остаются эквивалентными и сохраняют исходный порядок.

Задайте те же правила с помощью компараторов ключевых путей Foundation

Foundation предоставляет KeyPathComparator для сравнения свойств и sorted(using:) для применения последовательности компараторов. Первый компаратор задаёт основной порядок; последующие уточняют порядок равных элементов. Эти API доступны начиная с macOS 12. KeyPathComparator, sorted(using:)

Для эквивалентной версии на Foundation задайте два ключа позиции: ранг отсутствующего значения и числовую позицию. Благодаря рангу отсутствующие позиции окажутся в конце. Если ранг совпал, числовое значение, подставленное вместо отсутствующей позиции, не может совпасть с заданным нулём, поскольку у этих элементов различаются ранги.

Добавьте следующее расширение и вычисление после полной программы выше. Этот пример зависит от уже определённых в ней ShapeItem, items и expectedIDs. Он требует macOS 14 или новее, поскольку явно создаёт ComparableComparator с помощью init(order:). Доступность инициализатора

import Foundation

extension ShapeItem {
    var missingPositionRank: Int {
        effectivePosition == nil ? 1 : 0
    }

    var numericPositionKey: Int {
        effectivePosition ?? 0
    }
}

if #available(macOS 14.0, *) {
    let comparators: [KeyPathComparator<ShapeItem>] = [
        KeyPathComparator(\ShapeItem.group, order: .forward),
        KeyPathComparator(\ShapeItem.missingPositionRank,
                          order: .forward),
        KeyPathComparator(\ShapeItem.numericPositionKey,
                          order: .forward),
        KeyPathComparator(
            \ShapeItem.title,
            comparator: ComparableComparator<String>(order: .forward)
        ),
        KeyPathComparator(\ShapeItem.id, order: .forward)
    ]

    let foundationOrdered = items.sorted(using: comparators)
    precondition(foundationOrdered.map(\.id) == expectedIDs)
    print(foundationOrdered.map(\.id))
} else {
    print(items.sorted(by: comesBefore).map(\.id))
}

Явный компаратор заголовка сохраняет ранее выбранный стандартный строковый порядок Swift. Согласно документации Foundation, ComparableComparator использует реализацию Comparable сравниваемого типа, а KeyPathComparator принимает заданный компаратор свойства. ComparableComparator, Инициализатор компаратора ключевого пути

Для одного фиксированного порядка замыкание делает видимой каждую ветвь. Массив компараторов удобен, когда нужно представить последовательность поддерживаемых ключей как значения. В обоих вариантах оставьте выбор резервного значения в вычисляемом свойстве модели, чтобы обе реализации использовали одно определение.

Изменяйте правила, не меняя их смысл

Чтобы сортировать позиции по убыванию и при этом оставлять отсутствующие позиции в конце, измените только сравнение двух заданных позиций на left > right. В версии на Foundation меняйте направление только у numericPositionKey; для missingPositionRank оставьте прямой порядок. Разворот всего результата также изменит порядок групп, заголовков, идентификаторов и размещение отсутствующих значений.

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

Оставьте это небольшое вычисление синхронным. В руководстве Swift по конкурентности обычная синхронная сортировка отличена от точек приостановки, а предикат sorted(by:) в документации является синхронным. Если приложению нужны значения из асинхронной работы, получите их до вызова сортировщика. Добавить await в замыкание сравнения нельзя: сигнатура этого не поддерживает. Конкурентность, Сигнатура sorted(by:)

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

Источники