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

前端 未结 5 1048
野趣味
野趣味 2020-12-29 04:16

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 objec

相关标签:
5条回答
  • 2020-12-29 04:29

    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)
        }
    
        // `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 {
    
    }
    
    0 讨论(0)
  • 2020-12-29 04:38

    Here's another protocol-only (i.e. no base-class) solution but it simplifies how you use it while still making it opt-in. You use constrained extensions to adhere to both the Hashable and Equatable protocols.

    Several implementations will use a class extension to adopt protocol conformance, like this...

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

    But instead, we reverse it by extending the protocol itself, then using a constraint of AnyClass which makes this implementation work for all classes that simply specify conformance to the protocol, no class-specific implementation needed...

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

    With the above both in place, we can now do this...

    class Foo : Hashable {} // Defining with the class definition
    var fooToStringLookup:[Foo:String] = [:]
    
    class Laa {}
    extension Laa : Hashable {} // Adding to an existing class via an extension
    var laaToIntLookup:[Laa:Int] = [:]
    

    Of course Hashable also gives you Equatable implicitly, so these all now work too...

    let a = Foo()
    let b = a
    let msg = (a == b)
        ? "They match! :)"
        : "They don't match. :(" 
    print(msg)
    

    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.

    Going a step further, and speaking only about Equatable, if you want to make all object types implement Equatable and adhere to reference-semantics implicitly--something I personally wonder why it doesn't do this by default (and which you can still override per-type if needed)--you can use generics with a globally-defined equality operator, setting the constraint to AnyObject

    func == <T:AnyObject>(lhs: T, rhs: T) -> Bool {
        return lhs === rhs
    }
    
    func != <T:AnyObject>(lhs: T, rhs: T) -> Bool {
        return !(lhs == rhs)
    }
    

    Note: For completeness, you should also explicitly provide the != operator as unlike when defining the = operator in an extension, the compiler will not synthesize it for you.

    With that, now you can do this...

    class Laa {} // Note no protocols or anything else specified. Equality 'just works'
    
    let a = Laa()
    let b = a
    var msg = (a == b)
        ? "They match! :)"
        : "They don't match. :(" 
    print(msg)
    
    // Prints 'They match! :)'
    
    let c = Laa()
    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 Hee, based solely on id.

    class Hee {
        init(_ id:String){
            self.id = id
        }
        let id:String
    }
    
    // Override implicit object equality and base it on ID instead of reference
    extension Hee : Equatable {
    
        static func == (lhs:Hee, rhs:Hee) -> 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'
    
    0 讨论(0)
  • 2020-12-29 04:46

    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
        }
        
        /* Equatable protocol simply means establishing 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 so if we were to compare two instances (left-hand-side
           versus right-hand-side), we would compare their userId values. When
           it comes time to compare these objects with each other, the machine
           will look for this function and use it to make that determination. */
        static func == (lhs: CustomClass, rhs: CustomClass) -> Bool {
            return lhs.userId == rhs.userId
        }
        
        /* 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 in this protocol we must feed that property into a
           function which will produce the object's hash value. And again, userId
           makes the most sense because each instance carries a unique value. If
           userId was not unique, we could combine multiple properties to
           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
        }
    }
    
    0 讨论(0)
  • 2020-12-29 04:48

    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
        }
    }
    
    0 讨论(0)
  • 2020-12-29 04:49

    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
       }
    }
    
    0 讨论(0)
提交回复
热议问题