【发布时间】:2018-07-17 21:15:38
【问题描述】:
这很容易在 java 中完成 - 哈希码似乎是指向对象或其他东西的指针。为什么 swift 没有为我们提供同样的舒适,并要求我们自己定义函数?
【问题讨论】:
-
默认的 Java 哈希函数是可怕的,你几乎不想使用它。
-
“哈希码似乎是指向对象或其他东西的指针”后者。
这很容易在 java 中完成 - 哈希码似乎是指向对象或其他东西的指针。为什么 swift 没有为我们提供同样的舒适,并要求我们自己定义函数?
【问题讨论】:
哈希码似乎是一个指向对象或其他东西的指针。
在 Java 中描述 hashCode() (Object#hashCode()) 的默认实现时确实如此。
Java 提供hashCode() 的默认实现这一事实导致了我遇到的数百个错误。
在 Java 中,hashCode() 和 equals() 需要保持一致才能与基于哈希值的集合(如 HashMap 或 HashSet)一起使用。
在很多很多类中,equals() 的定义方式不是比较指向对象或其他东西的指针。 hashCode() 的默认实现永远不会与 equals() 保持一致,并导致某种难以发现的错误。
Swift 试图以比其他语言更强的方式告诉我们哈希值需要与类型的相等保持一致。
SE-0185 介绍了一种符合Equatable 和Hashable 的简单方法。在合适的条件下,平等是微不足道的,你不需要定义函数。
hashCode() 在 Java 中的默认实现是无用的,当你覆盖 equals() 时必须覆盖它,即使我们忘记覆盖 hashCode(),编译器也不会给我们任何警告。这真的是一种舒适吗?
【讨论】:
Java 唯一的值类型是一组固定的“原语”(bool、char、byte、short、int、long、float、double)。 Swift 有一个通用的机制来创建 Java 没有的值类型。使用标识(实例地址)作为 hashValue 的默认值的基础是没有意义的,因为值类型没有标识。
引用类型(类的实例,称为对象)通过复制指向对象的指针来传递。如果你有一些对象a,取它的默认hashCode,然后将它作为参数p传递给函数p,那么p.hashCode()将与a.hashCode()相同,因为指针值被保留.
值类型(结构、元组和枚举的实例)通过复制其内容或value 来传递。如果您有一些实例a,并尝试根据a 的成员开始的基地址派生hashCode,您将获得一些价值。但是,当您将它作为参数p 传递给函数f 时,您将导致a 的成员的副本进入堆栈中为p 预留的内存中f 中。这与a 的位置不同。如果您尝试根据p 的成员开始的基地址尝试派生hashCode,您将获得不同的值!
因此,值类型不存在身份。带有0b00000001 模式的Int8 与其他地方的Int8 模式具有完全相同的值,后者也是0b00000001。这与两个堆对象不同,它们每个都可以具有相同的内容,但通过它们不同(但伪永久)的位置来区分,这构成了它们身份的基础。
有一个开放的JDK enhancement proposal (#169) 用于引入通用值类型,因为它在减少诸如Optional<T>、Point 等小型短期对象的数量方面具有非常明显的性能优势。我怀疑它们会遇到斯威夫特也遇到了同样的障碍。以下是 JEP 的摘录(重点是我的):
总结
为处理不可变和 无引用对象,支持高效的按值计算 使用非原始类型。
...
说明
将定义一个新的运算符 lockPermanently,它接受一个对象 并将其标记为不可变且不可混淆。
一般情况下,永久锁定的对象不能受到约束 对任何依赖于参考身份的操作有意义 的对象。一个操作依赖于引用标识,如果 操作产生不同的结果取决于它是否适用 到原始对象或其克隆之一。因此:
- 字段和数组元素不能更改。
- 无法同步 执行。
- 无法调用等待或通知的方法。
- 无法查询身份哈希码。
- 不应执行指针相等检查。
【讨论】:
== 实现为=== 的情况。我当然无法想象这是一个明智的默认行为。
我已经发布了另一个答案 here on StackOverflow,它介绍了如何通过简单地为 == 和 != 定义全局运算符来为所有类类型隐式添加 Equatable 而无需手动执行任何操作。
此外,我还展示了如何使所有类都实现Hashable,只需在要创建Hashable 的类型上指定协议,而无需实现散列函数。它使用一个扩展来为你实现它。
两者都已成为我创建的所有新项目的主要内容。
HTH!
【讨论】: