How does equality work for NSObject subclasses, and why do two “equal” objects stay distinct in a Set?

2.9.5 · L3 · Interview

Predict first

How many elements are in the set?

class Player: NSObject {
    let id: Int
    init(_ id: Int) { self.id = id }
}
print(Set([Player(1), Player(1)]).count)
  • 1
  • 2

Predict first

How many elements now?

class Player: NSObject {
    let id: Int
    init(_ id: Int) { self.id = id }
    override func isEqual(_ o: Any?) -> Bool { (o as? Player)?.id == id }
    override var hash: Int { id }
}
print(Set([Player(1), Player(1)]).count)
  • 1
  • 2

Mental model. Two employees with the same name are still different badges until HR agrees both the name check and the badge number match.

NSObject conforms to Equatable and Hashable through the Objective-C methods isEqual(_:) and hash. By default both use identity: two different instances are never equal. So Set([Player(1), Player(1)]) holds two elements even if their ids match.

To get value equality, override both isEqual(_:) and hash, using the same fields in each. Overriding only isEqual breaks the contract (equal objects with different hashes), and the set still keeps both. Declaring a Swift == on the subclass is not enough: Set, Dictionary and Objective-C collections go through hash and isEqual. This is the same rule as plain Hashable, with one extra place to get it wrong.

Without overrides, equality is identity, so two instances are two elements.

Both isEqual and hash now use id, so the two players collapse into one element. Remove hash and it goes back to 2.

Common traps

  • Overriding isEqual without hash. Equal objects hash differently, so sets and dictionaries misbehave.
  • Adding static func == only. The Objective-C collections never call it.

They might follow up with

  • How does NSSet differ from Set? It uses isEqual and hash too, and holds objects only.
  • When should a class be value-equal at all? When it represents a value, such as a model with an id, and not an entity with identity.

Related questions

  • 2.1.2 What must Hashable guarantee, and why does iteration order change between launches?
  • 2.9.1 What is toll-free bridging, and how do you convert between Swift and Foundation types?

← Which Foundation types are value types in Swift, which are classes…