Avatar ·

Как удалить дубликаты из массива в Swift? (С сохранением порядка)

Привет! Я новичок в Swift и столкнулся с проблемой. У меня есть массив строк, в котором могут встречаться повторяющиеся значения. Например: ```swift let names = ["Анна", "Борис", "Анна", "Виктор", "Борис", "Дмитрий"] ``` Мне нужно получить новый массив, в котором каждый элемент встречается только один раз, но при этом порядок элементов должен сохраниться (первое вхождение). То есть результат должен быть: `["Анна", "Борис", "Виктор", "Дмитрий"]`. Я знаю, что можно использовать `Set`, но он не гарантирует порядок элементов. Также пробовал `NSOrderedSet`, но это из Objective-C, и мне кажется, что в Swift должно быть более элегантное решение. Подскажите, как правильно удалить дубликаты из массива в Swift, желательно с использованием стандартной библиотеки, без подключения Foundation (если возможно). И чтобы код был коротким и читаемым. Заранее спасибо! <<>>

Решение с сохранением порядка через расширение Array

Лучший способ удалить дубликаты из массива с сохранением порядка — использовать метод reduce(into:_:) в сочетании с Set для отслеживания уже встреченных элементов. Вот готовое расширение для Array:

extension Array where Element: Hashable {
    func removingDuplicates() -> [Element] {
        var seen = Set<Element>()
        return filter { element in
            guard !seen.contains(element) else { return false }
            seen.insert(element)
            return true
        }
    }
}

// Использование:
let names = ["Анна", "Борис", "Анна", "Виктор", "Борис", "Дмитрий"]
let uniqueNames = names.removingDuplicates()
print(uniqueNames) // ["Анна", "Борис", "Виктор", "Дмитрий"]

Как это работает

  • Требование Hashable: Элементы массива должны соответствовать протоколу Hashable, чтобы их можно было хранить в Set. Все стандартные типы (String, Int, Double и т.д.) уже соответствуют этому протоколу.
  • Метод filter: Проходит по массиву в порядке исходной последовательности. Для каждого элемента проверяет, был ли он уже добавлен в seen.
  • Set для отслеживания: seen хранит уже встреченные элементы. Если элемент уже есть — он пропускается (возвращается false). Если нет — добавляется в seen и включается в результат.
  • Сложность O(n): Алгоритм работает за линейное время, так как операции вставки и проверки в Set выполняются за O(1) в среднем.

Альтернатива с reduce(into:)

Если вам не нравится filter, можно использовать reduce(into:_:) — результат будет тот же:

extension Array where Element: Hashable {
    func removingDuplicates() -> [Element] {
        var seen = Set<Element>()
        return reduce(into: [Element]()) { partialResult, element in
            guard !seen.contains(element) else { return }
            seen.insert(element)
            partialResult.append(element)
        }
    }
}

Если порядок не важен

Если порядок элементов не имеет значения, можно просто использовать инициализатор Set:

let uniqueNames = Array(Set(names))
print(uniqueNames) // ["Борис", "Дмитрий", "Анна", "Виктор"] — порядок случаен

Важные замечания

  • Расширение работает только с Hashable элементами. Для пользовательских структур нужно либо добавить соответствие Hashable, либо использовать другой подход (например, сравнивать по id).
  • Если массив большой, а дубликатов мало — filter с Set будет эффективнее, чем reduce(into:_:), так как не создает промежуточный массив.
  • Для массивов, которые могут быть изменены, можно создать мутирующую версию: mutating func removeDuplicates().

Это решение использует только стандартную библиотеку Swift, не требует Foundation и отлично справляется с задачей. Рекомендую добавить это расширение в ваш проект как utility-метод.

<<>>

Решение через NSOrderedSet (с Foundation)

Если вы не против подключить Foundation (что в iOS/macOS проектах обычно уже сделано), можно использовать NSOrderedSet — это самый короткий способ:

import Foundation

let names = ["Анна", "Борис", "Анна", "Виктор", "Борис", "Дмитрий"]
let uniqueNames = NSOrderedSet(array: names).array as! [String]
print(uniqueNames) // ["Анна", "Борис", "Виктор", "Дмитрий"]

Плюсы и минусы

  • Плюс: Очень коротко и понятно. Работает с любыми объектами, не требует Hashable.
  • Минус: Возвращает [Any], нужно явное приведение типа. Работает медленнее, чем чистое Swift-решение из-за моста к Objective-C.
  • Минус: Требует импорта Foundation, что не всегда удобно в серверном Swift или библиотеках.

Когда использовать

Этот подход отлично подходит для быстрых прототипов, скриптов или когда вы точно знаете, что Foundation уже подключен. Для production-кода на Swift я рекомендую первое решение — оно более производительное и типобезопасное.