Какие правила необходимо соблюдать при реализации протокола Hashable в Swift?

«Какие правила необходимо соблюдать при реализации протокола Hashable в Swift?» — вопрос из категории Swift Core, который задают на 10% собеседований IOS Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Основное правило (контракт Hashable): Если a == b, то a.hashValue == b.hashValue. Обратное не обязательно — разные объекты могут иметь одинаковый хеш (коллизия).

Современная реализация (Swift 4.2+):

struct Person: Hashable {
    let id: UUID
    let name: String
    let age: Int

    // 1. Реализация hash(into:)
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)      // Начинайте с наиболее уникального свойства
        hasher.combine(name)    
        hasher.combine(age)     
    }

    // 2. Реализация == (обязательно для Hashable)
    static func == (lhs: Person, rhs: Person) -> Bool {
        return lhs.id == rhs.id && 
               lhs.name == rhs.name && 
               lhs.age == rhs.age
    }
}

Критические правила:

1. Консистентность с Equatable

// НЕПРАВИЛЬНО — нарушает контракт
struct BadHashable: Hashable {
    let value: Int

    func hash(into hasher: inout Hasher) {
        hasher.combine(value % 10)  // Хеширует только последнюю цифру!
    }

    static func == (lhs: Self, rhs: Self) -> Bool {
        return lhs.value == rhs.value  // Сравнивает полное значение!
    }
    // 11 == 21 → true, но hash(11) != hash(21) ← НАРУШЕНИЕ!
}

2. Используйте Hasher правильно

// ХОРОШО — комбинируем все значимые свойства
func hash(into hasher: inout Hasher) {
    hasher.combine(property1)
    hasher.combine(property2)
    // Порядок важен! Разный порядок → разный хеш
}

// ПЛОХО — ручной расчет хеша
var hashValue: Int {
    return property1.hashValue ^ property2.hashValue  // Устаревший подход
}

3. Избегайте изменяемых свойств в хеше

struct MutableUser: Hashable {
    var id: UUID
    var name: String  // Изменяемое свойство!

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
        hasher.combine(name)  // ОПАСНО: хеш изменится при смене имени
    }
}

var user = MutableUser(id: uuid1, name: "Alice")
let set = Set([user])  // Хеш вычислен здесь
user.name = "Bob"      // Хеш изменился, но объект в Set остался
// Теперь set.contains(user) может вернуть false!

4. Для классов

class User: Hashable {
    let id: UUID
    var name: String

    init(id: UUID, name: String) {
        self.id = id
        self.name = name
    }

    // Хешируем по идентификатору (ссылочная семантика)
    func hash(into hasher: inout Hasher) {
        hasher.combine(ObjectIdentifier(self))
    }

    static func == (lhs: User, rhs: User) -> Bool {
        return lhs === rhs  // Сравнение по ссылке
    }
}

5. Исключение незначимых свойств

struct LogEntry: Hashable {
    let id: UUID
    let message: String
    let timestamp: Date  // Не включаем в хеш, если сравнение по id
    let debugInfo: [String: Any]  // Сложный тип, исключаем

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)  // Только уникальный идентификатор
    }

    static func == (lhs: LogEntry, rhs: LogEntry) -> Bool {
        return lhs.id == rhs.id  // Сравниваем только по id
    }
}

6. Производительность

// ХОРОШО — эффективный хеш
struct Point3D: Hashable {
    let x, y, z: Double

    func hash(into hasher: inout Hasher) {
        hasher.combine(x)
        hasher.combine(y)
        hasher.combine(z)
    }
}

// ПЛОХО — дорогой хеш
struct ExpensiveHash: Hashable {
    let data: [Int]

    func hash(into hasher: inout Hasher) {
        // Хешируем весь массив — может быть медленно
        hasher.combine(data)  
        // Лучше: hasher.combine(data.count) + первый элемент
    }
}

Автоматическая реализация:

// Swift 4.1+ генерирует Hashable автоматически, если:
// 1. Все свойства Hashable
// 2. Нет кастомной реализации ==
struct AutoHashable: Hashable {
    let id: Int
    let name: String
    let tags: [String]  // Array уже Hashable, если элемент Hashable
    // Компилятор сгенерирует и hash(into:), и ==
}

Тестирование реализации:

func testHashableConsistency() {
    let a = Person(id: uuid1, name: "John", age: 30)
    let b = Person(id: uuid1, name: "John", age: 30)

    assert(a == b, "Objects should be equal")
    assert(a.hashValue == b.hashValue, "Hash values should match")

    // Проверка на коллизии (опционально)
    let set = Set([a, b])
    assert(set.count == 1, "Set should contain only unique objects")
}