我遇到了很多在线示例,当他们尝试遵循Hashable
时,他们只考虑id
。例如https://www.raywenderlich.com/8241072-ios-tutorial-collection-view-and-diffable-data-source,https://medium.com/@JoyceMatos/hashable-protocols-in-swift-baf0cabeaebd,...
/// Copyright (c) 2020 Razeware LLC
///
/// Permission is hereby granted, free of charge, to any person obtaining a copy
/// of this software and associated documentation files (the "Software"), to deal
/// in the Software without restriction, including without limitation the rights
/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
/// copies of the Software, and to permit persons to whom the Software is
/// furnished to do so, subject to the following conditions:
///
/// The above copyright notice and this permission notice shall be included in
/// all copies or substantial portions of the Software.
///
/// Notwithstanding the foregoing, you may not use, copy, modify, merge, publish,
/// distribute, sublicense, create a derivative work, and/or sell copies of the
/// Software in any work that is designed, intended, or marketed for pedagogical or
/// instructional purposes related to programming, coding, application development,
/// or information technology. Permission for such use, copying, modification,
/// merger, publication, distribution, sublicensing, creation of derivative works,
/// or sale is expressly withheld.
///
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
/// THE SOFTWARE.
import UIKit
class Video: Hashable {
var id = UUID()
var title: String
var thumbnail: UIImage?
var lessonCount: Int
var link: URL?
init(title: String, thumbnail: UIImage? = nil, lessonCount: Int, link: URL?) {
self.title = title
self.thumbnail = thumbnail
self.lessonCount = lessonCount
self.link = link
}
// 1
func hash(into hasher: inout Hasher) {
// 2
hasher.combine(id)
}
// 3
static func == (lhs: Video, rhs: Video) -> Bool {
lhs.id == rhs.id
}
}
我想知道,这是否是符合Hashable
的正确方法?我以为我们应该考虑所有类成员变量?
例如,通过仅在id
/ func hash
中使用func ==
,将产生以下不良行为。
我们将遇到2个内容不同的对象,但是func ==
在比较2个内容不同的对象时将返回true。
struct Dog: Hashable {
let id = UUID()
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: Dog, rhs: Dog) -> Bool {
lhs.id == rhs.id
}
}
var dog0 = Dog(name: "dog", age: 1)
var dog1 = dog0
/*
dog0 is -5743610764084706839, dog, 1
dog1 is -5743610764084706839, dog, 1
compare dog0 with dog1 is true
*/
print("dog0 is \(dog0.hashValue), \(dog0.name), \(dog0.age)")
print("dog1 is \(dog1.hashValue), \(dog1.name), \(dog1.age)")
print("compare dog0 with dog1 is \(dog0 == dog1)")
dog1.name = "another name"
dog1.age = 9
// Same id, but different content!
/*
dog0 is -5743610764084706839, dog, 1
dog1 is -5743610764084706839, another name, 9
compare dog0 with dog1 is true
*/
print("dog0 is \(dog0.hashValue), \(dog0.name), \(dog0.age)")
print("dog1 is \(dog1.hashValue), \(dog1.name), \(dog1.age)")
print("compare dog0 with dog1 is \(dog0 == dog1)")
我想知道,仅考虑Hashable
来符合id
是正确的吗?
p / s
我尝试从Java之类的其他语言中寻找关于哈希码生成的一般建议。这就是他们流行的有效Java书籍中写的。
不要试图从哈希码中排除重要字段 计算以提高性能。而产生的哈希函数 可能运行速度更快,其质量较差可能会降低哈希表的性能 直至无法使用。特别是哈希 函数可能会遇到大量实例, 主要区别在于您选择忽略的区域。如果发生这种情况, 哈希函数会将所有这些实例映射到一些哈希代码,并且 应该在线性时间内运行的程序将改为二次运行 时间。这不仅仅是一个理论问题。在Java 2之前, 字符串散列函数,最多使用16个字符,这些字符均匀分布 整个字符串中,从第一个字符开始。对于大 层次结构名称的集合,例如URL,此功能 完全显示了先前描述的病理行为。
答案 0 :(得分:3)
TL; DR:此哈希函数不是必需的,但合法且可以说是理想的。尽管在教程中很常见,但此==是不正确的,因为它完全破坏了Equatable所要求的可替代性,完全符合您的建议。
但是,正如哑光所指出的,无论如何,可变数据源可能都需要这样做。这样做没有好处,但是可能有必要。 (请务必阅读下面matt的所有评论。它们提供了很多重要的背景信息。在具体介绍可扩散数据源时,请参见他的回答;我对可扩散数据源并不特别熟悉。)
我建议参考文档,其中列出了内容。
首先,Hashable:
散列值意味着将其基本组成部分馈入由Hasher类型表示的散列函数。基本组件是那些有助于该类型的Equatable实现的组件。两个相等的实例必须以相同的顺序将相同的值提供给
hash(into:)
中的Hasher。
最重要的是,Hashable必须与Equatable保持一致。两件事必须永远不相等,但是具有不同的哈希值。
反之则不成立。两个不相等的事物具有相同的散列是完全有效的。实际上,这是称为pigeonhole principle的哈希的基本事实。良好的哈希可以避免不必要的相等性检查,从而提高性能。但是以下hash(into:)
函数始终有效:
func hash(into hasher: inout Hasher) {
hasher.combine(0)
}
这仅意味着每个值都具有相同的哈希,因此系统将始终调用==。这对性能不利(并且在服务器应用程序中可能转换为拒绝服务攻击,称为哈希洪泛)。但这是合法的。
如果这是合法的,则肯定只是对id
进行散列是合法的。
但是......
这使我们进入Equatable and its docs,并且最重要的段落(添加了重点):
相等意味着可替代性-相等比较的任何两个实例可以在取决于其值的任何代码中互换使用。 为保持可替换性,==运算符应考虑Equatable类型的所有可见方面。不建议公开Equatable类型的非值方面,而不是类标识,并且应明确指出暴露的任何非值方面。在文档中。
只有在任何情况下它们都可以相互替代时,才必须将其视为相等,并且不会影响程序的正确性。显然,在您的示例中,这是不正确的。实际上,对于具有可变公共属性的类型,这永远是不正确的(尽管许多教程都弄错了)。因此,您的==不正确。但是您的哈希函数很好,可以说是理想的。其目标是快速检查不平等,以最大程度地减少冲突。如果id相同,则仍然必须检查其余的值,但是如果它们不同,则知道不会相等。
如果您的Dog类型是不可变的(name
和age
是let
而不是var
),则以这种方式实现==是可以接受的。手动设置id
是不可能的,因此不可能获得两个具有相同id
但值不同的值。但是除非您可以显着提高性能,否则我不会这样做。它把正确性挂在太微妙的要求上。例如,如果一个扩展名添加了一个init
并允许直接设置id
,它将使您的==无效。 IMO太脆弱了。
私有可变状态如何?只要这仅出于性能目的(内存/缓存),就可以不使用==(和哈希)。但是,如果内部状态可以影响外部可见的行为,则它必须成为==的一部分。
好消息是,大多数时候您不需要担心。 Swift的自动实现可以立即为您正确处理此问题,并比较所有属性。因此,在您的Dog示例中,最好的解决方案是仅删除方法(我确定您已经意识到;只针对正在阅读的人们说出来)。只要有可能,我强烈建议对Hashable使用默认的一致性,并避免编写自己的一致性。
但是在必须自己实现的情况下,规则很简单:
指南也很简单:散列应该很快,同时最大程度地减少冲突。
我为==的这些错误实现看到的一个论据是试图使Set
正常工作。 IMO,这是对Set和Equatable的滥用,并且不能保证以预期的方式工作(如果您插入具有相同标识符但属性不同的重复值,则不确定哪些值将在集合中)。您不应该为了使用特定的数据结构而扭曲Equatable。您应该使用符合您含义的数据结构。
在通常情况下,正确的工具是Dictionary as [ID: Value]
。它表达了您的真正意思:一个ID与该ID的单个值之间的映射,而不是无序的唯一值包。
使用字典而不是集合可能会消耗内存(因为您必须重复ID)。但是,只有在证明存在要解决的问题之后,您才应该尝试解决此问题。
此外,请参阅下面的亚光评论。我没有花很多时间使用新的可扩散数据源。我记得当我第一次看到他们时,我担心他们可能会滥用Equatable。如果是这样,那么您可能不得不滥用Equatable来使用它们,这将解释一些使用此方法的教程。这并不能使它成为Swift的好习惯,但是Apple框架可能需要它。
随着我对Apple代码的研究更加深入(请参阅matt的答案),我注意到它们都遵循我上面讨论的规则:它们是不可变的,并且您不能在初始化期间设置UUID。这种构造使得两个值不可能具有相同的ID,而其他值却不同,因此检查ID总是足够的。但是,如果您使值可变,或者您将id设为let id = UUID()
以外的其他值,那么这种构造就很危险。
答案 1 :(得分:2)
那很好。对Hashable仅有一个要求:如果a == b
,则a.hashValue == b.hashValue
也必须为true。在此已完成,因此您的结构将用作字典键或作为集合成员。
请注意,如果您的hash(into:)
没有将任何数据(或仅恒定数据)合并到哈希器中,这也可以实现。这将使哈希表查找变慢,但是它们仍然可以工作。
另一种选择是比较==
实现中的所有字段,但仅将其中的一部分用于hash(into:)
中的散列。那仍然遵循规则(当然不允许其他方式)。这可能对性能优化很有用,但也可能会损害性能。取决于要散列的数据的分布。
答案 2 :(得分:1)
仅使用属性子集来实现Hashable
是否正确完全取决于您的要求。
如果对于某个对象,相等性实际上仅由单个变量(或变量的子集)定义,则对Hashable
(和Equatable
使用该变量子集是正确的一致性)。
但是,如果需要使用类型的所有属性来确定两个实例是否相等,则应该使用所有属性。
答案 3 :(得分:0)
具有多个属性的类型(包括UUID)是很好的,其中对Hashable和Equatable的一致性仅取决于UUID,而不取决于其他任何属性。 Apple在自己的代码中使用此模式。从此处下载Apple的示例代码:
https://docs-assets.developer.apple.com/published/6840986f9a/ImplementingModernCollectionViews.zip
查看WiFiController.Network结构,MountainsController.Mountain结构,OutlineViewController.OutlineItem类和InsertionSortArray.SortNode结构。他们都做同样的事情。因此,所有这些代码都是 Apple:
struct Network: Hashable {
let name: String
let identifier = UUID()
func hash(into hasher: inout Hasher) {
hasher.combine(identifier)
}
static func == (lhs: Network, rhs: Network) -> Bool {
return lhs.identifier == rhs.identifier
}
}
struct Mountain: Hashable {
let name: String
let height: Int
let identifier = UUID()
func hash(into hasher: inout Hasher) {
hasher.combine(identifier)
}
static func == (lhs: Mountain, rhs: Mountain) -> Bool {
return lhs.identifier == rhs.identifier
}
func contains(_ filter: String?) -> Bool {
guard let filterText = filter else { return true }
if filterText.isEmpty { return true }
let lowercasedFilter = filterText.lowercased()
return name.lowercased().contains(lowercasedFilter)
}
}
class OutlineItem: Hashable {
let title: String
let subitems: [OutlineItem]
let outlineViewController: UIViewController.Type?
init(title: String,
viewController: UIViewController.Type? = nil,
subitems: [OutlineItem] = []) {
self.title = title
self.subitems = subitems
self.outlineViewController = viewController
}
func hash(into hasher: inout Hasher) {
hasher.combine(identifier)
}
static func == (lhs: OutlineItem, rhs: OutlineItem) -> Bool {
return lhs.identifier == rhs.identifier
}
private let identifier = UUID()
}
struct SortNode: Hashable {
let value: Int
let color: UIColor
init(value: Int, maxValue: Int) {
self.value = value
let hue = CGFloat(value) / CGFloat(maxValue)
self.color = UIColor(hue: hue, saturation: 1.0, brightness: 1.0, alpha: 1.0)
}
private let identifier = UUID()
func hash(into hasher: inout Hasher) {
hasher.combine(identifier)
}
static func == (lhs: SortNode, rhs: SortNode) -> Bool {
return lhs.identifier == rhs.identifier
}
}
答案 4 :(得分:-1)
是真的。您的代码对散列有一个要求,当您使用dog.id == dog1.id
时,它仅比较dog == dog1
。
如果要检查struct的所有字段,请在==
方法中比较该字段。
static func == (lhs: Dog, rhs: Dog) -> Bool {
lhs.id == rhs.id && lhs.name == rhs.name && lhs.age == rhs.age
}
答案 5 :(得分:-1)
在我看来,您对Equatable
的询问比对Hashable
的询问要多。罗伯·纳皮尔(Rob Napier)为您提供了一个很棒的答案。
教程中的==
函数不正确(尽管它可能满足用例的要求)。应该把它排除在外了。
但是一旦您降低Equatable
,这是一个很好的默认值...
public extension Hashable where Self: Identifiable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
…尤其是对于引用类型,您可以为其使用另一个可爱的默认值。
public extension Equatable where Self: AnyObject {
static func == (class0: Self, class1: Self) -> Bool {
class0 === class1
}
}