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

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

Сначала определите, что означает «уникальность», а затем выбирайте коллекцию

Массивы Swift сохраняют порядок и допускают дубликаты. Множества содержат уникальные значения, но не имеют определённого порядка. Поэтому преобразование массива во множество, а затем обратно в массив не сохраняет порядок первых вхождений. Последующая сортировка задаёт порядок сортировки, который может отличаться от порядка добавления. Такое поведение коллекций описано в разделе Swift Типы коллекций.

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

Вопрос о правилах Правило для этого вычисления
Что определяет порядок входных данных? Позиция в переданном массиве
Когда значения считаются дубликатами? Совпадают фигура и размер
Какое вхождение сохраняется? Первое вхождение
Перемещает ли более поздний дубликат сохранённое значение? Нет
Что происходит с пустым входным списком? Возвращается пустой массив и сумма, равная нулю
Какие значения входят в сумму? Только сохранённые значения

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

Создайте на Mac полноценный локальный пример на Swift

Вам понадобится установленный инструментарий Swift. В Terminal выполните команду swift --version, чтобы убедиться, что она доступна. В официальном руководстве Swift Создание инструмента командной строки описаны эта проверка, создание исполняемого пакета и его запуск.

Создайте для примера новую директорию и инициализируйте исполняемый пакет:

mkdir OrderDemo
cd OrderDemo
swift package init --name OrderDemo --type executable

Замените сгенерированный файл исходного кода исполняемой программы в Sources на программу целиком, приведённую ниже. Имя сгенерированного файла и структура директорий могут различаться в зависимости от версии инструментария; замените существующую точку входа, а не добавляйте вторую. В этой программе используется точка входа @main, поэтому поместите её в файл OrderDemo.swift, при необходимости переименовав сгенерированный файл main.swift. В руководстве по командной строке Swift объясняется выбор между файлом main.swift и точкой входа @main.

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

struct Tile: Hashable {
    let shape: String
    let size: Int
}

func uniqueFirst<Value, Key: Hashable>(
    _ values: [Value],
    by key: (Value) -> Key
) -> [Value] {
    var seen: Set<Key> = []
    var result: [Value] = []

    for value in values {
        if seen.insert(key(value)).inserted {
            result.append(value)
        }
    }

    return result
}

@main
struct OrderDemo {
    static func main() {
        let input = [
            Tile(shape: "circle", size: 12),
            Tile(shape: "square", size: 8),
            Tile(shape: "circle", size: 12),
            Tile(shape: "circle", size: 20),
            Tile(shape: "square", size: 8)
        ]

        let retained = uniqueFirst(input, by: { $0 })
        let total = retained.reduce(0) { $0 + $1.size }
        let skipped = input.count - retained.count

        let expected = [
            Tile(shape: "circle", size: 12),
            Tile(shape: "square", size: 8),
            Tile(shape: "circle", size: 20)
        ]
        precondition(retained == expected)
        precondition(total == 40)
        precondition(skipped == 2)

        let empty: [Tile] = []
        precondition(uniqueFirst(empty, by: { $0 }).isEmpty)
        precondition(
            uniqueFirst([input[0], input[0]], by: { $0 })
                == [input[0]]
        )

        let byShape = uniqueFirst(input, by: { $0.shape })
        precondition(byShape == [input[0], input[1]])
        let shapeTotal = byShape.reduce(0) { $0 + $1.size }
        precondition(shapeTotal == 20)

        for tile in retained {
            print("\(tile.shape): \(tile.size)")
        }
        print("Total: \(total)")
        print("Skipped duplicates: \(skipped)")
        print("Shape-only total: \(shapeTotal)")
    }
}

Выполните эту команду из директории пакета:

swift run OrderDemo

Ожидаемый вывод программы после сообщений о сборке приведён ниже. Программа была проверена по полученной документации API, но здесь не запускалась, поскольку в этой исследовательской среде нет исполняемого файла Swift.

circle: 12
square: 8
circle: 20
Total: 40
Skipped duplicates: 2
Shape-only total: 20

Почему подход с массивом и множеством сохраняет порядок?

Единственный источник порядка вывода — массив. Цикл последовательно проходит по каждой позиции входных данных, а append(_:) помещает принятое значение в конец результата. Множество лишь отвечает на вопрос, встречался ли выбранный ключ ранее; программа его не перебирает.

Ключевое выражение — seen.insert(key(value)).inserted. В документации Swift указано, что insert(_:) возвращает inserted в виде true для нового элемента и false, если равный элемент уже присутствует. Существующий элемент при этом не меняется. Это возвращаемое значение позволяет одной операцией решить, нужно ли добавить исходное значение. См. документацию Apple о добавлении в множество.

Для заданных входных данных первый круг и квадрат принимаются. Второй идентичный круг пропускается. Больший круг принимается, поскольку его полное значение отличается, а повторный квадрат пропускается. Таким образом, остаются размеры 12 + 8 + 20 = 40.

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

Как выразить равенство пользовательских значений Swift?

Объявление struct Tile: Hashable запрашивает автоматически синтезируемое соответствие протоколу. Оба хранимых свойства, String и Int, хешируемы, поэтому Swift может синтезировать необходимое поведение. Условия такого синтеза Apple описывает в документации Hashable.

Использование { $0 } в качестве ключа означает, что уникальность определяется всем значением Tile. Использование { $0.shape } меняет правило: сохраняется по одной записи для каждой фигуры. В примере при таком подходе удаляется более поздний круг размером 20, и сумма уменьшается до 20. Это намеренное сравнение двух правил, а не вторая реализация одного и того же правила.

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

Если вы реализуете равенство и хеширование вручную, используйте в обоих случаях один и тот же набор существенных свойств. Apple требует, чтобы для равных экземпляров хешеру передавались одни и те же значения в одном и том же порядке. Не используйте hashValue как ключ уникальности: хеш не заменяет правило равенства значений. Передайте множеству настоящий хешируемый ключ, чтобы оно применяло это правило. См. требования к соответствию Hashable.

Когда лучше использовать упорядоченную коллекцию?

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

Вариант Подходящие условия Важная особенность
Массив и множество Один проход по синтетическим значениям Swift, возможно, с выбранным ключом Ваша функция задаёт поведение при первом вхождении
OrderedSet из Swift Collections Постоянное использование упорядоченной коллекции уникальных хешируемых значений Требуется отдельный пакет Swift Collections
NSOrderedSet из Foundation API уже работает с упорядоченными объектными коллекциями Foundation Класс представляет статическую упорядоченную коллекцию; для изменяемых операций используется NSMutableOrderedSet

В официальной документации Swift Collections OrderedSet описан как упорядоченная коллекция уникальных элементов, импортируемая через OrderedCollections. В ней есть операции, учитывающие позиции, например append(_:) и insert(_:at:). Это API пакета, поэтому одного import OrderedCollections недостаточно, чтобы настроить проект без зависимостей. См. OrderedSet.

В документации Foundation от Apple NSOrderedSet описан как статическая упорядоченная коллекция уникальных объектов, а NSMutableOrderedSet назван его изменяемым аналогом. Для приведённого выше вычисления над значениями Swift обобщённая функция напрямую выражает выбранный ключ равенства и не требует этой объектной коллекции. См. NSOrderedSet.

Что изменится, если вычисление станет частью интерфейса для настольной системы?

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

В интерфейсе macOS должным образом изолируйте состояние и обновления, связанные с интерфейсом. Apple описывает MainActor как глобальный актор, исполнитель которого эквивалентен главной очереди диспетчеризации; в руководстве по параллелизму Swift объясняется применение @MainActor к функциям и типам, которые обращаются к состоянию интерфейса. См. MainActor и Параллелизм.

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

Если в будущей версии появятся независимые синтетические вычисления, выполняемые параллельно, сохраняйте вместе с каждым результатом исходную позицию, после сбора результатов восстановите их порядок по позициям входных данных, а затем выполните проход для удаления дубликатов. Такой подход следует из описанной особенности порядка завершения. Оставьте текущий небольшой пример синхронным, пока дополнительная работа не оправдает более сложную координацию.

Проверьте правила перед повторным использованием функции

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

При адаптации проверьте три условия: у входных данных должен быть осмысленный порядок, ключ должен выражать предполагаемое правило равенства, а сохранение первого вхождения должно быть уместным. Функция намеренно пропускает последующие совпадающие элементы: она не объединяет их размеры и не заменяет сохранённое значение. Выводимое количество пропусков наглядно показывает этот эффект.

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

Источники