Всегда ли Set в Swift гарантирует уникальность элементов?

Ответ

Нет, не всегда. Set гарантирует уникальность элементов, но эта гарантия зависит от корректной реализации протоколов Hashable и Equatable для типа элемента.

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

  1. Set использует hashValue для быстрого поиска элемента в бакете.
  2. Если хэши совпадают, вызывается оператор == для точной проверки равенства.

Проблема возникает при некорректной реализации: если hash(into:) и == используют не все значимые свойства объекта, разные объекты могут считаться одинаковыми, и Set их отбросит.

Пример некорректной реализации:

struct User: Hashable {
    let id: Int
    let name: String

    // ОШИБКА: Сравнение только по имени, игнорируется `id`
    static func == (lhs: User, rhs: User) -> Bool {
        return lhs.name == rhs.name
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(name) // Хэшируется только имя
    }
}

let userSet: Set<User> = [
    User(id: 1, name: "Alex"),
    User(id: 2, name: "Alex") // Set воспримет это как дубликат и оставит только одного "Alex"
]
print(userSet.count) // 1, хотя id разные

Best Practice: Всегда включайте в hash(into:) и == все свойства, определяющие уникальность экземпляра.

Ответ 18+ 🔞

А, ну это классика, блядь! Сейчас объясню на пальцах, как эта штука работает, а то многие думают, что Set — это такая волшебная коробочка, куда кидаешь что угодно, а она сама разберётся.

Так вот, нет, сука! Не всегда!

Эта гарантия уникальности — она не с потолка берётся. Она целиком и полностью зависит от тебя, от того, как ты реализуешь для своего типа два протокола: Hashable и Equatable. Если накосячишь там — пиши пропало, Set тебя обманет, как последнего лоха.

Как оно, блядь, под капотом работает:

  1. Сначала Set смотрит на hashValue твоего объекта. Это как быстрый предфильтр, чтобы не сравнивать всё подряд, а сразу откинуть заведомо разных.
  2. Если хэши совпали (а они могут совпасть у разных объектов, это называется коллизия, ёпта), тогда уже вступает в дело оператор ==. Вот тут уже идёт точная, дотошная проверка: «А ты точно такой же, сука, или только похож?»

И вот тут-то и начинается пиздец, если ты реализовал эти штуки криво. Представь, что hash(into:) и == используют не все свойства, которые делают твой объект уникальным. Тогда два РАЗНЫХ объекта Set посчитает ОДНИМ И ТЕМ ЖЕ и просто выкинет один нахуй!

Смотри, какой прикол может быть:

struct User: Hashable {
    let id: Int // Уникальный айдишник, ёбаный
    let name: String

    // ОШИБКА, БЛЯДЬ! Сравниваем только по имени, про `id` забыли!
    static func == (lhs: User, rhs: User) -> Bool {
        return lhs.name == rhs.name // "Ну и чё, что id разные? Имена-то одинаковые!"
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(name) // Хэшируем тоже только имя, айдишник в пизду
    }
}

let userSet: Set<User> = [
    User(id: 1, name: "Alex"),
    User(id: 2, name: "Alex") // Set посмотрит на это и скажет: "О, второй Алекс? Да пошёл ты нахуй, дубликат!"
]
print(userSet.count) // Выведет 1, ёбта! Хотя мы-то знаем, что там два разных чела!

Вот так из-за кривых рук и получается, что уникальность — хуйня. Золотое правило, блядь, запомни раз и навсегда: в hash(into:) и в операторе == всегда включай ВСЕ свойства, которые определяют, чем один экземпляр твоего типа отличается от другого. Не выёбывайся, не пытайся быть умнее компилятора, а то сам себя и обманешь в итоге.