Ответ
Основное правило (контракт 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")
}