Xcode 10 GM: hash(into:) issue from NSObject class

Cocoa defines its own hashing API that is independent of Swift's Hashable. To customize hashing in NSObject subclasses, the correct property to override is NSObject.hash.

Trying to customize hashing by overriding hash(into:) and/or hashValue will not work correctly -- if you do that then NSDictionary, NSSet, and Foundation's other hashing collections won't work correctly with your class, often leading to severe problems.

It's rather unfortunate that hash and hashValue have distinct names; they are all too easy to confuse. In earlier Swift versions, NSObject.hashValue was accidentally left overridable, which added to the confusion.

Existing code that overrides NSObject.hashValue has been broken from the start; it needs to be fixed. To help us fix existing code and to prevent further problems, Swift 4.2 emits a deprecation warning for NSObject.hashValue overrides, and defines NSObject.hash(into:) non-overridable.

This doesn't mean that you can't use Hasher for such subclasses. For example, here is the proper way to define HashClass with custom hashing:

import Foundation

public final class HashClass: NSObject {
  var i: Int = 0

  public override func isEqual(_ other: Any) -> Bool {
    guard let other = other as? HashClass else { return false }
    return self.i == other.i
  }

  public override var hash: Int {
    var hasher = Hasher()
    hasher.combine(i)
    return hasher.finalize()
  }
}

Important note: If you override NSObject.hash, you must also override isEqual(_:). This isn't currently enforced, but forgetting to do so is another source of subtle but deadly bugs.

Any isEqual(_:) override you provide will get used by both Swift's == operator and Objective-C code that works with NSObjects.

8 Likes