Метод Set содержит разные значения в разное время

Я думал о том, как Swift обеспечивает уникальность для Set, потому что я бесплатно превратил один из моих объектов с Equatable в Hashable, и поэтому я придумал эту простую игровую площадку.

struct SimpleStruct: Hashable {
    let string: String
    let number: Int

    static func == (lhs: SimpleStruct, rhs: SimpleStruct) -> Bool {
        let areEqual = lhs.string == rhs.string
        print(lhs, rhs, areEqual)
        return areEqual
    }
}

var set = Set<SimpleStruct>()
let first = SimpleStruct(string: "a", number: 2)
set.insert(first)

Итак, мой первый вопрос был:

Будет ли вызываться метод static func == всякий раз, когда я вставляю новый объект в набор?

Мой вопрос исходит из этой мысли:

Для Equatable obj, чтобы принять это решение, единственный способ убедиться, что два obj одинаковы, — это запросить результат static func ==.

Для Hashable obj более быстрый способ - сравнить hashValues... но, как и в моем случае, реализация по умолчанию будет использовать как string, так и number, в отличие от логики ==.

Итак, чтобы проверить, как ведет себя Set, я только что добавил оператор печати.

Я понял, что иногда я получал заявление о печати, иногда нет. Типа иногда hashValue недостаточно, чтобы принять это решение... Так что метод вызывался не каждый раз. Странный...

Итак, я попытался добавить два объекта, которые равны, и мне интересно, что будет результатом set.contains

let second = SimpleStruct(string: "a", number: 3)
print(first == second) // returns true
set.contains(second)

И чудеса чудес, запустив пару раз площадку, я получил разные результаты и это могло привести к непредсказуемым результатам... Добавление

var hashValue: Int {
    return string.hashValue
}

он избавляется от любых неожиданных результатов, но я сомневаюсь:

Почему без пользовательской реализации hashValue иногда вызывается ==, а иногда нет? Должна ли Apple избегать такого неожиданного поведения?

Возвращает false возвращает true


person ndPPPhz    schedule 02.12.2018    source источник
comment
Ваша реализация нарушает правило, согласно которому одинаковые экземпляры должны иметь одинаковое хеш-значение. Поэтому следует ожидать «неожиданного поведения»…   -  person Martin R    schedule 02.12.2018
comment
Разве ваша функция == не должна сравнивать и строку, и число? Прямо сейчас у вас могут быть две структуры с одной и той же строкой, но разными числами, но ваш == рассматривает их как равные. Это действительно то, чего вы хотите?   -  person rmaddy    schedule 02.12.2018
comment
Просто чтобы было понятно. Мой вопрос был: почему static func == иногда вызывается, а иногда нет. Конечно, если реализация hashValue и == использует разные свойства, у меня будет неожиданное поведение, но мне интересно, почему иногда hashValue недостаточно, и компилятор должен запросить ==, чтобы получить правильный ответ.   -  person ndPPPhz    schedule 02.12.2018
comment
Грубо говоря, набор использует несколько «сегментов» для хранения своих элементов. Хэш-значение используется для определения того, в какой корзине будет храниться значение. Затем каждый элемент в корзине сравнивается с == со значением, которое вы ищете. – Но это детали реализации, и они могут измениться в любой момент.   -  person Martin R    schedule 02.12.2018


Ответы (1)


Синтезированная реализация требования Hashable использует все сохраненные свойства struct, в вашем случае string и number. Ваша реализация == основана только на строке:

let first = SimpleStruct(string: "a", number: 2)
let second = SimpleStruct(string: "a", number: 3)

print(first == second) // true
print(first.hashValue == second.hashValue) // false

Это нарушение требования протокола Hashable:

Два одинаковых экземпляра должны передавать одинаковые значения в Hasher в виде хэша (в:) в одном и том же порядке.

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

Что вероятно происходит в вашем тесте, так это то, что хеш-значение second используется для определения «сегмента» набора, в котором будет храниться значение. Это может быть или не быть тем же сегментом, в котором хранится first. — Но это деталь реализации: неопределенное поведение — это неопределенное поведение, оно может привести к неожиданным результатам или даже к ошибкам во время выполнения.

Реализация

var hashValue: Int {
    return string.hashValue
}

или альтернативно (начиная с Swift 4.2)

func hash(into hasher: inout Hasher) {
    hasher.combine(string)
}

исправляет нарушение правил и, следовательно, заставляет ваш код вести себя так, как ожидалось.

person Martin R    schedule 02.12.2018