【发布时间】:2016-04-14 19:58:36
【问题描述】:
在 Java 之后我开始学习 swift。在 Java 中,我可以使用任何对象作为 HashSet 的键,因为它具有基于对象标识符的默认 hashCode 和 equals。如何在 Swift 中实现相同的行为?
【问题讨论】:
在 Java 之后我开始学习 swift。在 Java 中,我可以使用任何对象作为 HashSet 的键,因为它具有基于对象标识符的默认 hashCode 和 equals。如何在 Swift 中实现相同的行为?
【问题讨论】:
如果您使用的是类而不是结构,则可以使用ObjectIdentifier 结构。请注意,您还必须为您的班级定义== 以符合Equatable(Hashable 需要它)。它看起来像这样:
class MyClass: Hashable { }
func ==(lhs: MyClass, rhs: MyClass) -> Bool {
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
class MyClass: Hashable {
var hashValue: Int {
return ObjectIdentifier(self).hashValue
}
}
【讨论】:
func hash(into hasher: inout Hasher) 而不是 hashValue,所以它看起来像:func hash(into hasher: inout Hasher) { return hasher.combine(ObjectIdentifier(self))
在 Swift 中,类型必须符合 Hashable 和 Equatable 才能在 Dictionary 或 Set 等数据结构中使用。但是,您可以通过使用对象的“对象标识符”来添加“自动一致性”。在下面的代码中,我实现了一个可重用的类来自动执行此操作。
注意,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)
}
}
要使用,只需使用您的类和子类HashableClass,然后一切都应该正常工作!
class MyClass: HashableClass {
}
【讨论】:
不是使用Hashable 协议扩展您的类,然后为每个类复制实现,而是扩展Hashable 协议本身,将其限制为AnyObject,然后将共享实现放在那里。这样一来,只要让你的类符合Hashable,它们就会自动选择共享的实现。
如此处接受的答案所示,一些实现将在其类类型上使用扩展来实现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
}
}
那是很多重复的代码!
我的建议是扭转局面。不是扩展单个类类型,而是扩展Hashable 协议本身,然后将其限制为AnyClass 并将实现放在那里。这样做会自动将该实现应用于所有类,这些类只是指定符合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
}
}
注意:虽然您可以将相等运算符直接添加到
Hashable扩展,但通过将其应用于Equatable的扩展(Hashable隐式符合),您可以使用相同的技术来应用实例相等即使您不想要或不需要散列性,也可以对类型进行分类。
以上两点都做好了,我们现在可以这样做了……
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! :)
注意:这不会干扰直接实现
Hashable的类,因为特定于类的定义更明确,因此优先,它们可以和平共存。
更进一步,只谈论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)
}
注意:如果你走这条路,为了完整起见,你还应该像我在这里所做的那样定义
!=运算符。与在扩展中定义==运算符不同,编译器不会自动为您合成它,因此您必须明确地执行此操作。
有了以上内容,您现在可以执行此操作了...
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
}
}
注意:如果您要覆盖相等性的对象也实现了
Hashable,您必须确保相应的哈希值也相等,如定义,相等的对象应该产生相同的哈希值。
也就是说,如果您将本文顶部的 Hashable 扩展名限制为 AnyObject,并且只需使用 Hashable 标记您的类,您将获得基于对象身份的默认实现,因此对于共享相同 ID(因此被认为相等)的不同类实例将不匹配,因此您必须明确确保也实现散列函数。 编译器不会为你捕捉到这个。
同样,只有当你重写相等性并且你的类实现了Hashable时,你才需要这样做。如果是这样,这里是如何做到这一点...
在 Hee 上实现 hashable 以遵循相等/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 准系统
对于使类可散列的准系统实现,我们必须同时遵守Equatable 和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
}
}
【讨论】:
另一种选择是为AnyObject 实现Hashable 和Equatable 协议的扩展。好像和你在Java中提到的效果差不多。
它将默认行为添加到项目中的所有类,与仅将此类行为添加到指定类相比,它并不是更好的选择。所以,为了完整起见,我只是提到它:
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 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
}
}
【讨论】: