Как обрабатывать хэш-коллизии для словарей в Swift

TLDR

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

фон

я уже задавал этот вопрос как реализовать протокол хэширования в Swift для массива Int (пользовательская строковая структура). Позже я добавил мой собственный ответ, которым похоже, сработало.

однако недавно я обнаружил тонкую проблему с hashValue конфликты при использовании Dictionary.

самый простой пример

я упростил код, насколько это возможно, до следующего примера.

пользовательские структуры

struct MyStructure: Hashable {

    var id: Int

    init(id: Int) {
        self.id = id
    }

    var hashValue: Int {
        get {
            // contrived to produce a hashValue collision for id=1 and id=2
            if id == 1 {
                return 2 
            }
            return id
        }
    }
}

func ==(lhs: MyStructure, rhs: MyStructure) -> Bool {
    return lhs.hashValue == rhs.hashValue
}

обратите внимание на глобальную функцию для перегрузки оператора равенства ( = = ), чтобы соответствовать Equatable Протокол, который требует протокол Hashable.

тонкая ключевая проблема словаря

если я создам Dictionary С MyStructure как ключ

var dictionary = [MyStructure : String]()

let ok = MyStructure(id: 0)            // hashValue = 0
let collision1 = MyStructure(id: 1)    // hashValue = 2
let collision2 = MyStructure(id: 2)    // hashValue = 2

dictionary[ok] = "some text"
dictionary[collision1] = "other text"
dictionary[collision2] = "more text"

print(dictionary) // [MyStructure(id: 2): more text, MyStructure(id: 0): some text]
print(dictionary.count) // 2

равные значения хэша вызывают collision1 ключ перезаписывается collision2 ключ. Нет никакого предупреждения. Если такое столкновение произошло только один раз в словаре со 100 ключами, то его можно легко пропустить. (Мне потребовалось довольно много времени, чтобы заметить эту проблему.)

очевидное проблема со словарем literal

если я повторю это со словарным литералом, проблема становится гораздо более очевидной, потому что возникает фатальная ошибка.

let ok = MyStructure(id: 0)            // hashValue = 0
let collision1 = MyStructure(id: 1)    // hashValue = 2
let collision2 = MyStructure(id: 2)    // hashValue = 2

let dictionaryLiteral = [
    ok : "some text",
    collision1 : "other text",
    collision2 : "more text"
]
// fatal error: Dictionary literal contains duplicate keys

вопрос

у меня сложилось впечатление, что это не было необходимо для hashValue всегда возвращает уникальное значение. Например, Мэтт Томпсон говорит,

одним из самых распространенных заблуждений о реализации пользовательского хэша функция приходит от ... думая, что хэш-значения должны быть разными.

и уважаемый так пользователь @Gaffa говорит этот один из способов обработки хэш-коллизий -

считайте хэш-коды не уникальными и используйте компаратор равенства для фактические данные для определения уникальности.

по-моему, вопрос должны ли хэш-функции протокола swift Hashable возвращать уникальные значения? не на момент написания этой книги я получил адекватный ответ.

после прочтения Swift Dictionary вопрос как обрабатываются хэш-коллизий?, я предположил, что Swift автоматически обрабатывает хэш-столкновения с Dictionary. Но, по-видимому, это не так, если я использую пользовательский класс или структуру.

комментарий заставляет меня думать, что ответ заключается в том, как реализуется Equatable протокол, но я не уверен, как я должен изменить он.

func ==(lhs: MyStructure, rhs: MyStructure) -> Bool {
    return lhs.hashValue == rhs.hashValue
}

эта функция вызывается для каждого поиска ключа словаря или только при столкновении хэша? (Обновление: см. этот вопрос)

что мне делать, чтобы определить уникальность, когда (и только когда) происходит хэш-столкновение?

4 ответов


func ==(lhs: MyStructure, rhs: MyStructure) -> Bool {
    return lhs.hashValue == rhs.hashValue
}

обратите внимание на глобальную функцию для перегрузки оператора равенства ( = = ), чтобы соответствовать эквивалентному протоколу, который требуется протоколом Hashable.

ваша проблема является неправильным равенство реализация.

хэш-таблица (например, словарь Swift или набор) требует отдельного равенство и хэш реализаций.

хэш получает вас близко к объект, который вы ищете; равенство получает вам точный объект, который вы ищете.

ваш код использует ту же реализацию для хэш и равенство, и это гарантирует столкновение.

чтобы устранить проблему, реализуйте равенство чтобы соответствовать точным значениям объекта (однако ваша модель определяет равенство). Например:

func ==(lhs: MyStructure, rhs: MyStructure) -> Bool {
    return lhs.id == rhs.id
}

я думаю, что у вас есть все части головоломки, вам нужно просто собрать их вместе. У тебя куча отличных источников.

хэш-коллизии в порядке. Если происходит хэш-столкновение, объекты будут проверены на равенство (только для объектов с соответствующими хэшами). По этой причине объекты' Equatable соответствие должно быть основано на чем-то другом, кроме hashValue, Если вы не уверены, что хэши не могут столкнуться.

это точно причина, что объекты, которые соответствуют Hashable должен также соответствовать Equatable. Swift нужен более специфичный для домена метод сравнения, когда хэширование не сокращает его.

в том же статья NSHipster, вы можете увидеть, как Mattt реализует isEqual: и Person класса. В частности, у него есть isEqualToPerson: метод, который проверяет другие свойства человека (дата рождения, полное имя), чтобы определить равенство.

- (BOOL)isEqualToPerson:(Person *)person {
  if (!person) {
    return NO;
  }

  BOOL haveEqualNames = (!self.name && !person.name) || [self.name isEqualToString:person.name];
  BOOL haveEqualBirthdays = (!self.birthday && !person.birthday) || [self.birthday isEqualToDate:person.birthday];

  return haveEqualNames && haveEqualBirthdays;
}

он не использовать хэш-значение при проверке равенства - он использует свойства, характерные для его класса.

аналогично, Swift не позволяет вам просто использовать Hashable объект как ключ словаря -- неявно, по наследованию протокола -- ключи должны соответствовать Equatable Как хорошо. Для стандартной библиотеки типов Свифт об этом уже позаботились, но для пользовательских типов и класса, вы должны создать свой собственный == реализация. Вот почему Swift не обрабатывает словарь автоматически столкновения с пользовательскими типами-необходимо реализовать Equatable сами!

в качестве прощальной мысли, Mattt также заявляет, что вы часто можете просто сделать проверку личности, чтобы убедиться, что ваши два объекта находятся на разных адресах памяти, и, следовательно, разные объекты. В Swift, что бы хотелось вот так:

if person1 === person2 {
    // ...
}

здесь нет никакой гарантии, что person1 и person2 имеют разные свойства, просто они занимают отдельное пространство в памяти. И наоборот, в более раннем isEqualToPerson: метод, нет никакой гарантии, что два человека с одинаковыми именами и датами рождения на самом деле те же люди. Таким образом, вы должны учитывать, что имеет смысл для вас конкретный тип объекта. Опять же, еще одна причина, по которой Swift не реализует Equatable для вас на пользовательских типах.


равные значения хэша приводят к тому, что ключ collision1 перезаписывается ключ collision2. Нет никакого предупреждения. Если такое столкновение только случилось однажды в словаре с 100 ключами, тогда это могло быть легко пропущенный.

хэш-столкновение не имеет к этому никакого отношения. (Хэш-коллизии никогда не влияют на результат, только на производительность.) Он работает точно так, как задокументировано.

Dictionary операции работы на равенство (==) ключей. Словари не содержат дубликатов ключей (то есть ключей, которые равны). Когда вы устанавливаете значение с ключом, он перезаписывает любую запись, содержащую равный ключ. Когда вы получаете запись с индексом, она находит значение с ключом, которое равно, а не обязательно совпадает с тем, что вы дали. И так далее.

collision1 и collision2 равны (==), основываясь на том, как вы определили == оператора. Таким образом, установка записи с ключом collision2 необходимо перезаписать любую запись с ключом collision1.

П. С. То же самое относится и словари на других языках. Например, в какао,NSDictionary не позволяет дублировать ключи, т. е. ключи, которые isEqual:. В Java, Maps не позволяют дублировать ключи, т. е. ключи, которые .equals().


вы можете увидеть мои комментарии к ответам на этой странице и этой ответ. Я думаю, что все ответы по-прежнему написаны очень запутанным образом.

tl; dr 0) вам не нужно писать реализацию isEqual ie == между хэш-значениями. 1) только обеспечить/возврата hashValue. 2) просто реализовать Equatable как обычно

0) чтобы соответствовать hashable у вас должно быть вычисленное значение с именем hashValue и дайте ему соответствующее значение. в отличие от equatable протоколом сравнение из hashValues является уже там. Вы НЕ нужно написать:

func ==(lhs: MyStructure, rhs: MyStructure) -> Bool {
    return lhs.hashValue == rhs.hashValue
    // Snippet A
}

1) тогда он использует hashValue чтобы проверить, существует ли для этого индекса hashValue (вычисленного по его модулю против количества массива) ключ, который ищут, или нет. Он смотрит внутри массива пар ключ/значение этого индекса.

2) как отказоустойчивый ie в случае, если там are соответствующие хэши вы возвращаетесь к обычному == func. (Логически это необходимо из-за отказоустойчивости. Но вам также это нужно, потому что протокол Hashable соответствует Equatable и поэтому вы должны написать реализацию для ==. В противном случае вы получите компилятор ошибка)

func == (lhs: MyStructure, rhs: MyStructure) -> Bool {
    return lhs.id == rhs.id
    //Snippet B
}

вывод:

вы должны включить фрагмент B, исключить фрагмент A, а также иметь hashValue свойства