Подкласс NSObject в Swift: hash vs hashValue, isEqual vs ==

При подклассификации NSObject в Swift следует переопределить хэш или реализовать Hashable? Кроме того, следует ли переопределить isEqual: или реализовать ==?

Ответ 1

NSObject уже соответствует протоколу Hashable:

extension NSObject : Equatable, Hashable {
    /// The hash value.
    ///
    /// **Axiom:** `x == y` implies `x.hashValue == y.hashValue`
    ///
    /// - Note: the hash value is not guaranteed to be stable across
    ///   different invocations of the same program.  Do not persist the
    ///   hash value across program runs.
    public var hashValue: Int { get }
}

public func ==(lhs: NSObject, rhs: NSObject) -> Bool

Я не мог найти официальную ссылку, но кажется, что hashValue вызывает метод hash из NSObjectProtocol, а == вызывает isEqual: (из того же протокола). См. обновление на конец ответа!

Для подклассов NSObject правильный путь, по-видимому, для переопределения hash и isEqual:, и вот эксперимент, который демонстрирует, что:

1. Переопределить hashValue и ==

class ClassA : NSObject {
    let value : Int

    init(value : Int) {
        self.value = value
        super.init()
    }

    override var hashValue : Int {
        return value
    }
}

func ==(lhs: ClassA, rhs: ClassA) -> Bool {
    return lhs.value == rhs.value
}

Теперь создайте два разных экземпляра класса, которые считаются "равно" и поместите их в набор:

let a1 = ClassA(value: 13)
let a2 = ClassA(value: 13)

let nsSetA = NSSet(objects: a1, a2)
let swSetA = Set([a1, a2])

print(nsSetA.count) // 2
print(swSetA.count) // 2

Как вы можете видеть, оба NSSet и Set рассматривают объекты как разные. Это не желаемый результат. Массивы также имеют неожиданные результаты:

let nsArrayA = NSArray(object: a1)
let swArrayA = [a1]

print(nsArrayA.indexOfObject(a2)) // 9223372036854775807 == NSNotFound
print(swArrayA.indexOf(a2)) // nil

Установка контрольных точек или добавление отладочного вывода показывает, что переопределенная Оператор == никогда не вызывается. Я не знаю, если это ошибка или предполагаемое поведение.

2. Переопределить hash и isEqual:

class ClassB : NSObject {
    let value : Int

    init(value : Int) {
        self.value = value
        super.init()
    }

    override var hash : Int {
        return value
    }

    override func isEqual(object: AnyObject?) -> Bool {
        if let other = object as? ClassB {
            return self.value == other.value
        } else {
            return false
        }
    }
}

Для Swift 3, определение isEqual: изменилось на

override func isEqual(_ object: Any?) -> Bool { ... }

Теперь все результаты ожидаются:

let b1 = ClassB(value: 13)
let b2 = ClassB(value: 13)

let nsSetB = NSSet(objects: b1, b2)
let swSetB = Set([b1, b2])

print(swSetB.count) // 1
print(nsSetB.count) // 1

let nsArrayB = NSArray(object: b1)
let swArrayB = [b1]

print(nsArrayB.indexOfObject(b2)) // 0
print(swArrayB.indexOf(b2)) // Optional(0)

Обновление: Поведение теперь задокументировано в Взаимодействие с API Objective-C в разделе "Использование Swift с Cocoa и Objective-C":

Класс NSObject выполняет сравнение идентичности, поэтому вы должны реализовать свой собственный метод isEqual: в классах, которые происходят из класса NSObject.

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

Ответ 2

Внедрите Hashable, который также требует, чтобы вы выполняли оператор == для вашего типа. Они используются для большого количества полезных материалов в стандартной библиотеке Swift, такой как функция indexOf, которая работает только с коллекциями типа, который реализует Equatable или тип Set<T>, который работает только с типами, которые реализуют Hashable.