Swift,如何实现基于对象引用的Hashable协议?

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

我在 Java 之后开始学习 swift。在 Java 中,我可以使用任何对象作为 HashSet 的键,因为它具有基于对象标识符的默认 hashCodeequals。如何在 Swift 中实现相同的行为?

如果您使用的是 classes 而不是结构,则可以使用 ObjectIdentifier 结构。请注意,您还必须为 class 定义 == 以符合 EquatableHashable 要求)。它看起来像这样:

class MyClass: Hashable { }

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

class MyClass: Hashable {
    var hashValue: Int {
        return ObjectIdentifier(self).hashValue
    }
}

在Swift中,类型必须符合HashableEquatable才能用于数据结构,例如Dictionary或[=15] =].但是,您可以通过使用对象的“对象标识符”来添加“自动一致性”。在下面的代码中,我实现了一个可重用的 class 来自动执行此操作。

请注意,Swift 4.2 更改了 Hashable 的实现方式,因此您不再覆盖 hashValue。相反,您覆盖 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)
    }
}

要使用,只需带上您的 class 和子 class HashableClass,然后一切正常!

class MyClass: HashableClass {

}

另一种选择是为 AnyObject 实现对 HashableEquatable 协议的扩展。好像和你在Java.

中提到的效果差不多

它为您项目中的所有 class 添加默认行为,与仅向指定的 class 添加此类行为相比,它不是更好的选择。所以,为了完整起见,我只是提一下:

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

现在,如果您想为 class 实现特定的逻辑,您仍然可以这样做,如下所示:

class HashableClass { //deleted the Hashable conformance


}
extension HashableClass : Hashable{

   func hash(into hasher: inout Hasher) {

      //your custom hashing logic
   }
}

为避免向 AnyObject 添加默认行为,可以声明另一个协议并添加仅与此新协议相关的扩展:

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

长话短说:

不是使用 Hashable 协议扩展 classes,然后为每个协议复制实现,而是扩展 Hashable 协议本身,将其限制为 AnyObject,然后将共享实现放在那里。这样一来,只需让您的 classes 符合 Hashable,他们就会自动选择该共享实现。

详情...

如此处接受的答案所示,一些实现将使用其 class 类型的扩展来实现 Hashable。该方法的问题是您必须为您定义的每种类型复制实现,这违反了 DRY(即不要重复自己)原则。

这是该方法的一个示例...

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

重复代码太多了!

我的建议是颠倒过来。不是扩展单个 class 类型,而是扩展 Hashable 协议本身,然后将其限制为 AnyClass 并将实现放在那里。这样做会自动将该实现应用到 all classes,它们只是简单地指定符合 Hashable 协议,没有 class-需要具体实施。

这是这种方法的样子...

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.

有了以上两点,我们现在可以这样做了...

extension SomeClass : Hashable {}
extension SomeOtherClass : Hashable {}
extension YetAnotherClass : Hashable {}

无重复码。只是遵守协议。

当然如前所述,Hashable 也隐含地为您提供 Equatable,所以这些现在都可以工作了...

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,如果你想 隐含地(即没有手动一致性到所需的协议)使所有对象类型实现 Equatable 使用身份来测试相等性——我个人想知道为什么默认情况下不是这种情况(如果需要,你仍然可以覆盖每个类型)—— - 您可以将泛型与全局定义的相等运算符一起使用,再次将约束设置为 AnyObject

这是代码...

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.

有了以上内容,您现在可以执行此操作...

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! :)

如上所述,您仍然可以使用等式的特定类型版本。这是因为它们优先于 AnyObject 版本,因为它们更具体,因此可以与上面提供的默认引用相等性和平共存。

这是一个假设上面的例子,但仍然为 Laa 定义了一个明确的相等版本,完全基于 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.

就是说,如果您将 Hashable 扩展名限制为此 post 顶部的 AnyObject,只需将您的 class 标记为 Hashable 您将获得基于对象身份的默认实现,因此它不会匹配共享相同 ID(因此被视为相等)的不同 class 实例,因此您必须明确确保实现哈希功能也一样。 编译器不会为您捕获这个

同样,只有在您的 class 实现 Hashable 覆盖等式 时,您才需要这样做。如果是这样,请按以下步骤操作...

Hee 上实现可散列以遵循 equality/hashable 关系:

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

最后,这是你如何使用它...

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'

Swift 5个准系统

对于使 class 可散列的准系统实现,我们必须同时遵守 EquatableHashable 协议。如果我们非常了解我们的对象,我们就可以确定使用哪些 属性 或属性来使其可等化和可散列。

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