Swift, how to implement Hashable protocol based on object reference?
Asked Answered
A

5

48

I've started to learn swift after Java. In Java I can use any object as a key for HashSet, cause it has default hashCode and equals based on object identifier. How to achieve the same behaviour in Swift?

Agadir answered 10/1, 2016 at 13:12 Comment(1)
Is this what you are looking for #30346200 ?Socket
C
62

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 { 

    static func == (lhs: MyClass, rhs: MyClass) -> Bool {
        ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(ObjectIdentifier(self))
    }

}
Cattima answered 10/1, 2016 at 13:24 Comment(1)
Starting from Swift 4.2 you must implement func hash(into hasher: inout Hasher) instead of hashValue, so it would look like: func hash(into hasher: inout Hasher) { return hasher.combine(ObjectIdentifier(self))Pliner
G
37

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 {

}
Glossal answered 27/5, 2017 at 17:6 Comment(3)
This changed my life when I had to implement Hashable protocol on custom class with only optional custom class attribute.Wivina
Xcode just showed the error "type xxx does not conform to Hashable", but wouldn't allow me to automatically add stubs or even tell me what was missing (it only added the static func == stub to conform to Equatable, but the error remained). Adding the function hash(into: inout Hasher) fixed it! ThanksBugger
Just a heads-up. You don't need to create the HashableClass to get this functionality for your own classes. You can get the same thing purely with protocols. Check out my answer here on the same page to see how, but the TL;DR is you extend the Hashable protocol itself, restricting your extension only to classes, then you put the shared implementation there. Then you can simply add Hashable to your own class and it automatically get that same implementation you mentioned here for free, no custom base-class needed.Expeller
E
21

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 by default.

Note: Even with this extension, you still/always have the option to implement custom implementations for any of your classes as needed. This is because extensions targeting specific classes always supersede extensions targeting AnyClass, and class-specific implementations always supercede any extension-defined implementations entirely.

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 on Equatable instead (which Hashable 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, if you want to implicitly make all object types support Equatable using their identity for comparison--something I personally wonder why Swift doesn't do by default (i.e. there's never a case when something would not be equal to itself), and which you can still override on a per-type basis if that's something you actually needed--you can use a globally-defined equality operator, using a generic constraint of AnyObject just as before.

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 explicitly define the != operator as I did above. This is because unlike when defining the == operator in an extension, the compiler will not synthesize this for you.

Additionally, it is strongly encouraged you define its implementation as simply returning the inverse of calling ==, and not by by trying to re-create that inverse logic manually! (In other words, my using == vs === above is not a typo! It's intentional.) This approach ensures you are using the true opposite of what it means to be considered equal, thus it's less prone to errors, especially if equality is determined by more complex logic.

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'
Expeller answered 7/3, 2020 at 17:48 Comment(5)
A well thought out and solid approach to doing this. This is my preferred answer.Karynkaryo
Note defining all objects as equatable if they are the same instance doesn't feel right. Equatable is about having equivalent data without necessarily being the same instance hence why there are separate operators == and ===Steadman
@NickMcConnell, I agree completely! And that's why instances should implicitly be equal to themselves since all of the data matches. Put another way, there's never a case where an instance should not be equal to itself. Now, if you want to relax the equality to allow different instances to match on data alone, then just as you have to do now, you explicitly define the equality operator for your types, comparing only what you care about, and that will supersede the instance-comparison since that type-comparison is more specialized. I showed exactly that in my answer above.Expeller
wrapping in type that has an id that can be compared seems the way to goLots
Yes, but be careful that the ID is somehow bound to the object identifier. If it's an entity identifier (e.g. an ID from a key column in a database), then it is possible to have two classes both with the same ID, but with other data that's different, and you would not want that to report as equal. (e.g. Person(id: 123, name="Joe"} should not be equal to Person(id: 123, name="Mama"}). If you use the object identifier, there's no way for differing instances to have/share the same ID so that issue is avoided.Expeller
N
9

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 requires us to establish a predicate that will
     determine if two instances of this type are equal or unequal based on
     on what we consider equal and unequal. Here, userId makes the most sense
     since we know they'll always be unique. And so now when we compare two
     instances of this type, this is the function that will make that
     comparison. */
    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 that will determine if two instances of this 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 since we know they'll always be unique.
       However, if userId was not unique, then we could combine multiple
       properties to (hopefully) generate a unique hash. Or we could simply
       generate a UUID within each instance and use that property solely. */
    func hash(into hasher: inout Hasher) {
        hasher.combine(userId)
        //hasher.combine(name) If userId was not unique, we could add this.
        //hasher.combine(count) If that's still not enough, we could add even more.
        
        // However, at this point, consider generating a UUID and using that.
    }
}
Newfashioned answered 17/9, 2020 at 15:37 Comment(1)
This should be the accepted solution. Great explanation. Thanks!Jacqui
P
4

Another option is to implement an extension to Hashable and Equatable protocols for AnyObject. It seems to achieve a similar effect as the one you mentioned in Java.

It adds a default behavior to all classes within your project and is not necessary the better option compared to adding such behavior to only a designated class. So, I am just mentioning it for the sake of completeness:

class HashableClass: Hashable {


}

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
   }
}

Now, if you you would like to implement specific logic for your class, you could still do that, like this:

class HashableClass { //deleted the Hashable conformance


}
extension HashableClass : Hashable{

   func hash(into hasher: inout Hasher) {

      //your custom hashing logic
   }
}

To avoid adding default behavior to AnyObject, another protocol can be declared and extension relevant only to this new protocol can be added:

protocol HashableClass : AnyObject{

}

class SomeClass: Hashable, HashableClass {


 }

 extension Hashable where Self: HashableClass{

   func hash(into hasher: inout Hasher) {

      hasher.combine(ObjectIdentifier(self))
   }
 }


extension Equatable where Self: HashableClass{

   static func ==(lhs: Self, rhs: Self) -> Bool {
     return lhs === rhs
   }
}
Punchinello answered 12/1, 2019 at 15:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.