**Implementing Hashable/Equatable in Swift** Kirk Spaziani # Introduction Swift 4.1 implemented my friend Tony Allevato's [SE-185](https://github.com/apple/swift-evolution/blob/master/proposals/0185-synthesize-equatable-hashable.md) proposal, automatically synthesizing `Hashable` and `Equatable` in structs and enums where all members conformed to those protocols. Swift 4.2 implemented [SE-206](https://github.com/apple/swift-evolution/blob/master/proposals/0206-hashable-enhancements.md), eliminating the need to be a mathematician to write a decent hash function. It is now easier than ever to conform to these protocols, so let's explore some corner cases that aren't magically covered. First, some background: Swift's `Hashable` protocol extends `Equatable`, indicating a close relationship. If two instances are equal, they must have the same hash value. The `Equatable` protocol defines what it means for two instances to be equal. ## Value Types Structs and enums are value types in Swift and can be considered to have value semantics, which implies a very clear definition of what equality means for these types. If two values are the same, the instances are equal. A reliable hash calculated from a value allows an instance to be quickly found in a `Set` or used as a `Key` in a `Dictionary`. ## Reference Types Classes are reference types in Swift, which means they are only indirectly related to a value. Reference Equality means two references pointing to the same object, rather than pointing to two different objects with the same value. Swift special cases reference equality with the `===` operator, allowing `===` to be used by classes without the classes implementing Equatable. Classes can also be subclassed, which can create arbitrary value relationships that are up to the programmer to define. # Objective-C/NSObject Since a large part of writing Swift is interoping with Objective-C, supporting equality and hashing in Swift based NSObject subclasses is likely to come up. `NSObject` provides default implementations of a method called `isEqual:` that does reference equality, and a property called `hash`, which just returns the address of the object. In order to provide value semantics in the `NSObject` world, `NSObject` subclasses have to override these methods and follow some sane set of rules for consistency. !!! Tip Remember that in Swift the `==` operator invokes `isEqual:` on `NSObject` subclasses and the `===` operator checks reference equality. Equality Type | Objective-C | Swift -------------------|-------------|------ Value Equality |isEqual: | == Reference Equality |== | === [Table [nsobject-operators]: `NSObject` equality in `Objective-C` and `Swift`] Let's take a quick look at NSObject equality checks, remember that the default implementation of `isEqual:` just checks reference equality. ~~~ let x = NSObject() let y = NSObject() let z = y x == y // isEqual:, false. x === y // Reference Equality, false. x == z // isEqual:, false. x === z // Reference Equality, false. y == z // isEqual:, true. y === z // Reference Equality, true. ~~~ [Example: `NSObject` Reference Equality] In order to provide value semantics to an `NSObject` subclass, it's necessary to overload `isEqual:` and provide an implementation that tests the value of an object. It is also necessary to override `hash`. We can also use Swift 4.2's `Hasher` to avoid funky hash math. ~~~ class TwoStrings: NSObject { let p1: String let p2: String init(p1: String, p2: String) { self.p1 = p1 self.p2 = p2 } public override func isEqual(_ object: Any?) -> Bool { // An NSObject instance and nil are never equal. guard let object = object else { return false } // Ensure that the object being tested is a TwoStrings or subclass. guard let rightChild = object as? TwoStrings else { return false } // Test reference equality. if self === rightChild { return true } // Finally compare all properties through a helper method. return isEqualToTwoStrings(rightChild) } private func isEqualToTwoStrings(_ twoStrings: TwoStrings) -> Bool { return p1 == twoStrings.p1 && p2 == twoStrings.p2 } public override var hash: Int { // Use Swift 4.2's hasher rather than calculating by hand, it's 2018! var hasher = Hasher() hasher.combine(p1) hasher.combine(p2) return hasher.finalize() } } let c1 = TwoStrings(p1: "omg", p2: "hax") let c2 = TwoStrings(p1: "hax", p2: "wow") let c3 = TwoStrings(p1: "hax", p2: "wow") let c4 = c3 x == c1 // isEqual:, false. y == c1 // isEqual:, false. c1 == c2 // isEqual:, false. c1 == c3 // isEqual:, false. c1 == c4 // isEqual:, false. c2 == c3 // isEqual:, true. c3 === c3 // Reference Equality, false. c2 == c4 // isEqual:, true. c2 === c4 // Reference Equality, false. c3 == c4 // isEqual:, true. c3 === c4 // Reference Equality, true. // Create a Swift Set containing all the objects declared above. let set: Set<TwoStrings> = [c1, c2, c3, c4] // Count is 2 because c3 and c4 have value equality two c2, so will be culled. set.count // 2 ~~~ [Example: Overriding `isEqual:` and `hash` in an `NSObject` subclass] Now let's consider the more complex cases. Suppose we create a new subclass of `TwoStrings` called `TwoCountyStrings` that has two strings but keeps a debugging count of some method's call for no good reason. This subclass adds no new properties that affect value equality, but instead adds functionality. `TwoStrings` instances and `TwoCountyStrings` instances should be able to be compared for equality based on their values. If possible, we should try to avoid subclassing for this purpose, trying instead to implement the additional functionality in an extension or through a different system. Why do we want to do this? Consider `NSString` and `NSMutableString`, if both have the value `@"Hello World"`, they should be equal, right? Swift's value semantics reduce the need for these types of cases, but this article is about those pesky corner cases. Consider also a subclass that manages some sort of state that doesn't affect the value, like a cache. ~~~ class TwoCountyStrings: TwoStrings { var newProperty: Int = 0 func printStuffAndCount() { newProperty += 1 print("Hello!") } } let tcs1 = TwoCountyStrings(p1: "omg", p2: "hax") let tcs2 = TwoCountyStrings(p1: "nope", p2: "nothing") tcs1 == c1 // true, same value. tcs1 === c1 // false, different instances. tcs1 == c2 // false, different values. tcs1 == tcs2 // false, different values. ~~~ [Example: A subclass with the same value semantics as its parent] That's it. The parent's `isEqual:` and `hash` are sufficient as is. The other subclass `ThreeStrings`, has an additional string and represents something different than `TwoStrings`, so an instance of `ThreeStrings` should never be equal to an instance of `TwoStrings`. This scenario should be avoided. Implementing `isEqual:` and calling super will result in potentially breaking the commutative property of equality `(if a == b then b == a)`, and efforts to prevent that quickly become unmaintainable. It's possible to easily do it by checking for an exact type match in the base class implementation, but then that would break `TwoCountyStrings` equality. # Swift Classes Pure Swift classes lack all of `NSObject`'s baggage, but still provide plenty of rope to hang yourself with. Everything I mentioned above should be considered when designing an inheritance hierarchy that involves value semantics. In fact, since Swift provides structs and enums, which are legitimate value types, I highly recommend using those for all of your value semantic needs and only use classes as a last resort. Suppose you wanted to delegate equality to an object of arbitrary type. `Equatable` and the Swift type system doesn't support defining something as simply `Equatable` to be compared against any arbitrary thing, so a different solution is required. Creating a class hierarchy around hashable values is one solution. ~~~ class Key: Hashable { public static func ==(lhs: Key, rhs: Key) -> Bool { // Call a virtual function. return lhs.equal(to: rhs) } // Virtual function to be overridden by subclass. func equal(to other: Key) -> Bool { // Do it the NSObject way: Reference Equals. return self === other } public func hash(into hasher: inout Hasher) { // Do it the NSObject way: Pointer Address as value. hasher.combine(Unmanaged.passUnretained(self).toOpaque()) } } let x = Key() let y = Key() let y1 = y x == y // false, different instances. y == y1 // true, points to the same instance. y.hashValue == y1.hashValue // true, same pointer generates same hash value. ~~~ Child classes should, for simplicity, define all their own properties and override `equal(to:)`, the new virtual method defined in `Key`, and `hash(into:)`. ~~~ class CoordinateKey: Key { let x: Int let y: Int init(x: Int, y: Int) { self.x = x self.y = y } override func equal(to other: Key) -> Bool { // Don't call super. // Require same type. guard let otherCoordinateKey = other as? CoordinateKey else { return false } // Compare all values. return x == otherCoordinateKey.x && y == otherCoordinateKey.y } public override func hash(into hasher: inout Hasher) { // Don't call super. hasher.combine(x) hasher.combine(y) } } let ck1 = CoordinateKey(x: 0, y: 25) let ck2 = CoordinateKey(x: 4, y: 7) let ck3 = CoordinateKey(x: 4, y: 7) let ck4 = ck3 let k = Key() ck1 == ck2 // false, different value. ck1 == ck3 // false, different value. ck1 == ck4 // false, different value. ck2 == ck3 // true, same value. ck2 === ck3 // false, different instance. ck2 == ck4 // true, same value. ck3 == ck4 // true, same value. ck3 === ck4 // true, same instance. k == ck1 // false, code correctly returns false. ~~~ This works, but the Swift type system also supports generics, and this same effect can be achieved incredibly simply with a generic struct. ~~~ struct Key : Hashable where T: Hashable { let value: T init(value: T) { self.value = value } } ~~~ Because of [SE-185](https://github.com/apple/swift-evolution/blob/master/proposals/0185-synthesize-equatable-hashable.md), `==` and `hash(into:)` are synthesized in the above example because the lone property, `value`, is constrained to be `Hashable`. # Conclusion Equality has always been a difficult concept in computing, and each language having different ideas on how to solve it doesn't help matters. Swift's improvements in this area are useful, but there are still a number of tricky corner cases, largely stemming from Objective-C/NSObject compatibility. Hopefully the examples I provided help guide you through the corner cases.