Что такое протокол Hashable в Swift и как он работает?

Ответ

Hashable — это протокол, который позволяет типу быть использованным в качестве ключа в Dictionary или элемента в Set. Он является наследником протокола Equatable.

Для чего нужен: Для эффективного хранения и поиска объектов в хеш-таблицах (лежащих в основе Set и Dictionary).

Основное требование: Должна быть реализована функция hash(into:), которая комбинирует хеш-значения всех значимых свойств типа.

Пример реализации для кастомной структуры:

struct User: Hashable {
    let id: UUID
    let username: String
    let email: String

    // Реализация Hashable (и Equatable) может быть синтезирована компилятором,
    // если все свойства соответствуют Hashable.
    // Ручная реализация требуется для выбора конкретных свойств.
    func hash(into hasher: inout Hasher) {
        // В хеш включаем только id и username, email игнорируем.
        hasher.combine(id)
        hasher.combine(username)
    }

    // Соответствующая реализация Equatable (обязательна для Hashable).
    static func == (lhs: User, rhs: User) -> Bool {
        return lhs.id == rhs.id && lhs.username == rhs.username
    }
}

// Использование в Set и Dictionary
let userSet: Set<User> = [User(id: uuid1, username: "alex", email: "a@test.com")]
let userScores: [User: Int] = [user1: 100]

Важнейшее правило (контракт Hashable):

Если два объекта равны (a == b), их хеш-значения обязаны быть равными (a.hashValue == b.hashValue).

Обратное НЕ верно: Разные объекты (a != b) могут иметь одинаковые хеш-значения (это коллизия). Качественная hash(into:) функция минимизирует вероятность коллизий.

Как работает Hasher: Swift использует детерминированный, но не криптографический хеш-алгоритм. Значение хеша может меняться между запусками программы (по умолчанию), что является мерой безопасности. Для тестов это поведение можно отключить через переменную окружения.

Автоматический синтез (Swift 4.1+): Для структур и перечислений, все свойства которых соответствуют Hashable, компилятор автоматически генерирует реализации hash(into:) и ==.

Ответ 18+ 🔞

А, ну вот, опять эти протоколы, блядь. Hashable, говоришь? Да это ж та самая штука, без которой тебе в Dictionary ключом не стать и в Set не залезть, как последнему мудаку. По сути, это такой пропуск в мир быстрого поиска, понимаешь? Как будто тебе дали билет, а ты его в хеш-функцию сунул, и он тебе номерок выдал — короткий и понятный.

Зачем это, спрашивается, нахуй? А затем, что искать по номерку — быстро, ёпта. Представь себе склад: у тебя миллион коробок. Искать по надписи «Вилка для глаз или в жопу раз» — долго. А если у каждой коробки есть короткий цифровой код, который ведёт прямо к ней? Вот это и есть хеш-таблица, а Hashable — это умение твоего типа такой код себе сгенерировать.

Что от тебя хотят? Реализовать одну ёбаную функцию — hash(into:). В неё ты должен запихнуть все значимые свойства, по которым ты хочешь, чтобы объект отличался. Как будто мешаешь ингредиенты в один котёл.

Вот смотри, пример, чтобы не быть мудаком:

struct User: Hashable {
    let id: UUID
    let username: String
    let email: String

    // Реализация Hashable (и Equatable) может быть синтезирована компилятором,
    // если все свойства соответствуют Hashable.
    // Ручная реализация требуется для выбора конкретных свойств.
    func hash(into hasher: inout Hasher) {
        // В хеш включаем только id и username, email игнорируем.
        hasher.combine(id)
        hasher.combine(username)
    }

    // Соответствующая реализация Equatable (обязательна для Hashable).
    static func == (lhs: User, rhs: User) -> Bool {
        return lhs.id == rhs.id && lhs.username == rhs.username
    }
}

// Использование в Set и Dictionary
let userSet: Set<User> = [User(id: uuid1, username: "alex", email: "a@test.com")]
let userScores: [User: Int] = [user1: 100]

Видишь? Я сказал хешеру: «Слушай сюда, дружок-пирожок, вот тебе id, вот тебе username. email — похуй, его не трогай». И теперь два юзера с одинаковым айдишником и ником будут считаться одинаковыми, даже если почты у них разные, блядь. Удивление пиздец, да?

А теперь главное правило, которое нарушать — себя не уважать:

Если два объекта равны по ==, то их хеш-значения обязаны совпадать. Точка, ёбаный насос.

Обратное, конечно, не всегда правда. Могут два разных объекта случайно получить одинаковый хеш — это называется коллизия, и это жизнь, ёпта. Но хорошая hash(into:) старается, чтобы такое случалось реже, чем ты находишь свои носки после стирки.

Про Hasher: Это такой хитрожопый мужик внутри Swift. Он берёт твои данные, мешает их в блендере и выдаёт число. Алгоритм детерминированный (в рамках одного запуска программы), но между разными запусками результат может отличаться — это для безопасности, чтобы всякие умники не догадались. Хотя для тестов можно эту фичу и отключить, если совсем припёрло.

И самое сладкое, блядь (Swift 4.1+): Если у тебя структура или enum, и все его свойства сами по себе Hashable, то можно вообще нихуя не писать! Компилятор сам всё сгенерирует, как по маслу. Просто объяви протокол, и всё — волшебство, ёбушки-воробушки. Но если нужно выбрать только часть свойств — тогда придётся ручками, как в примере выше.

Вот и вся философия. Не усложняй, просто корми хешер нужными полями и не нарушай контракт. А то получишь нестабильную коллекцию и будешь потом чесать репу, почему объект не находится.