Swift, how to implement Hashable protocol based on object reference?
In Swift, the type must conform to Hashable
and Equatable
for it to be used in a data structure such as a Dictionary
or a Set
. However, you can add "automatic conformance" by using the "object identifier" of the object. In the code below, I implemented a reusable class to do this automatically.
Note, Swift 4.2 changed how Hashable
is implemented, so you no longer override hashValue
. Instead, you override hash(into:)
.
open class HashableClass {
public init() {}
}
// MARK: - <Hashable>
extension HashableClass: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}
// `hashValue` is deprecated starting Swift 4.2, but if you use
// earlier versions, then just override `hashValue`.
//
// public var hashValue: Int {
// return ObjectIdentifier(self).hashValue
// }
}
// MARK: - <Equatable>
extension HashableClass: Equatable {
public static func ==(lhs: HashableClass, rhs: HashableClass) -> Bool {
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
}
To use, just take your class and subclass HashableClass
, then everything should just work!
class MyClass: HashableClass {
}
Swift 5 barebones
For a barebones implementation of making a class hashable, we simply must conform to both the Equatable
and Hashable
protocols. And if we know our object well, we can determine which property or properties to use to make it equatable and hashable.
class CustomClass: Equatable, Hashable {
let userId: String
let name: String
let count: Int
init(userId: String, name: String, count: Int) {
self.userId = userId
self.name = name
self.count = count
}
/* The Equatable protocol simply requires us to establish a predicate to
determine if two instances of the same type are equal or unequal based
on what we consider equal and unequal. In this class, userId makes
the most sense and so if we were to compare two instances of this class,
we would compare their userId values. And so when it comes time to
compare two instances anywhere in our process (the app), Swift will
look for this function and use it to make that determination. */
static func == (lhs: CustomClass, rhs: CustomClass) -> Bool {
return lhs.userId == rhs.userId
}
/* The Hashable protocol is similar to Equatable in that it requires us to
establish a predicate to determine if two instances of the same type
are equal or unequal, again based on what we consider equal and
unequal, but here we must feed that property (or properties) into a
function that will produce the object's hash value. And this makes sense
because the purpose of a hash is to serve as a unique identifier. And,
again, userId makes the most sense because each instance carries a unique
value. However, if userId was not unique, then we could combine multiple
properties to (hopefully) generate a unique hash. */
func hash(into hasher: inout Hasher) {
hasher.combine(userId)
//hasher.combine(name) If userId was not unique, we could have added this.
// And if that still doesn't do it, consider generating a random UUID for
// each instance and that can serve as the sole hashing property.
}
}
If you are working with classes and not structs, you can use the ObjectIdentifier
struct. Note that you also have to define ==
for your class in order to conform to Equatable
(Hashable
requires it). It would look something like this:
class MyClass: Hashable { }
func ==(lhs: MyClass, rhs: MyClass) -> Bool {
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
class MyClass: Hashable {
var hashValue: Int {
return ObjectIdentifier(self).hashValue
}
}
TL;DR:
Rather than extending your classes with the Hashable
protocol, then duplicating the implementation for each of them, you instead extend the Hashable
protocol itself, constraining it to AnyObject
, then you put the shared implementation there. That way, by simply conforming your classes to Hashable
, they will automatically pick up that shared implementation.
The Details...
As shown in the accepted answer here, several implementations will use extensions on their class types to implement Hashable
. The issue with that approach is you have to duplicate the implementation for every type you define violating DRY (i.e. Don't Repeat Yourself) principles.
Here's an example of that approach...
extension SomeClass : Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}
static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs === rhs
}
}
extension SomeOtherClass : Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}
static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs === rhs
}
}
extension YetAnotherClass : Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}
static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs === rhs
}
}
That's a lot of duplicated code!
What I propose is to reverse things. Instead of extending the individual class types, you extend the Hashable
protocol itself, then constrain it to AnyClass
and put the implementation there. Doing this automatically applies that implementation to all classes that simply specify conformance to the Hashable
protocol, no class-specific implementation needed.
Here's what this approach looks like...
extension Hashable where Self: AnyObject {
func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}
}
extension Equatable where Self: AnyObject {
static func == (lhs:Self, rhs:Self) -> Bool {
return lhs === rhs
}
}
Note: While you could add the equality operator directly to the
Hashable
extension, by applying it to an extension onEquatable
instead (whichHashable
implicitly conforms to) you can use the same technique to apply instance equality to class types even when you don't want or need hashability.
With the above both in place, we can now do this...
extension SomeClass : Hashable {}
extension SomeOtherClass : Hashable {}
extension YetAnotherClass : Hashable {}
No repeated code. Just a conformance to a protocol.
Of course as stated, Hashable
also gives you Equatable
implicitly, so these all now work too...
let a = SomeClass()
let b = a
let msg = (a == b)
? "They match! :)"
: "They don't match. :("
print(msg)
// Prints They match! :)
Note: This will not interfere with classes that implement
Hashable
directly as a class-specific definition is more explicit, thus it takes precedence and these can peacefully coexist.
Equatable Implicitly
Going a step further, and speaking only about Equatable
, if you want to implicitly (i.e. no manual conformance to a protocol needed) make all object types implement Equatable
using identity to test equality--something I personally wonder why it isn't the case by default (and which you can still override per-type if needed anyway)--you can use generics with a globally-defined equality operator, again setting the constraint to AnyObject
Here's the code...
func == <T:AnyObject>(lhs: T, rhs: T) -> Bool {
return lhs === rhs
}
func != <T:AnyObject>(lhs: T, rhs: T) -> Bool {
return !(lhs == rhs)
}
Note: If you go this route, for completeness, you should also define the
!=
operator as I did here. Unlike when defining the==
operator in an extension, the compiler will not automatically synthesize it for you so you must do this explicitly.
With the above in place you can now do this...
class Foo {} // Note no protocols or anything else specified. Equality 'just works' for classes
let a = Foo()
let b = a
var msg = (a == b)
? "They match! :)"
: "They don't match. :("
print(msg)
// Prints They match! :)
let c = Foo()
var msg = (a == c)
? "They don't match! :)"
: "They match. :("
print(msg)
// Prints They don't match! :)
As mentioned above, you can still use type-specific versions of equality as well. This is because they take precedence over the AnyObject
version as they are more specific, and thus can peacefully coexist with the default reference-equality provided above.
Here's an example that assumes the above is in place, but still defines an explicit version of equality for Laa
, based solely on id
.
class Laa {
init(_ id:String){
self.id = id
}
let id:String
}
// Override implicit object equality and base it on ID instead of reference
extension Laa : Equatable {
static func == (lhs:Laa, rhs:Laa) -> Bool {
return lhs.id == rhs.id
}
}
Caution: If the object you are overriding equality on also implements
Hashable
, you must ensure the corresponding hash values are also equal as by definition, objects that are equal should produce the same hash value.
That said, if you have the Hashable
extension constrained to AnyObject
from the top of this post, and simply tag your class with Hashable
you will get the default implementation which is based on the object's identity so it will not match for different class instances that share the same ID (and thus are considered equal), so you must explicitly make sure to implement the hash function as well. The compiler will not catch this for you.
Again, you only have to do this if you are overriding equality and your class implements Hashable
. If so, here's how to do it...
Implement hashable on Hee
to follow the equality/hashable relationship:
extension Hee : Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(id) // Easiest to simply use ID here since that's what Equatable above is based on
}
}
And finally, here's how you use it...
let hee1 = Hee("A")
let hee2 = Hee("A")
let msg2 = (hee1 == hee2)
? "They match! :)"
: "They don't match. :("
print(msg2)
// Prints 'They match! :)'
let set = Set<Hee>()
set.append(hee1)
set.append(hee2)
print("Set Count: \(set.count)")
// Prints 'Set Count: 1'