swift - 仅考虑id来遵循Hashable是否正确?

标签 swift hashable

我遇到了很多在线示例,当他们尝试遵循Hashable时,他们仅考虑id。例如https://www.raywenderlich.com/8241072-ios-tutorial-collection-view-and-diffable-data-sourcehttps://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书籍中所写的内容。

Do not be tempted to exclude significant fields from the hash code computation to improve performance. While the resulting hash function may run faster, its poor quality may degrade hash tables’ performance to the point where they become unusable. In particular, the hash function may be confronted with a large collection of instances that differ mainly in regions you’ve chosen to ignore. If this happens, the hash function will map all these instances to a few hash codes, and programs that should run in linear time will instead run in quadratic time. This is not just a theoretical problem. Prior to Java 2, the String hash function used at most sixteen characters evenly spaced throughout the string, starting with the first character. For large collections of hierarchical names, such as URLs, this function displayed exactly the pathological behavior described earlier.

最佳答案

TL; DR:此哈希函数不是必需的,但合法且可以说是理想的。尽管在教程中很常见,但此==是不正确的,因为它完全破坏了Equatable所要求的可替代性,正如您建议的那样。
但是,如无光泽的说明,无论如何,可扩散的数据源可能都需要这样做。这样做没有好处,但是可能有必要。 (请务必阅读下面matt的所有评论。它们提供了许多重要的上下文。在具体介绍可扩散数据源时,请参见他的回答;我对可扩散数据源并不特别熟悉。)

我建议引用文档,其中列出了这些内容。
首先,Hashable:

Hashing a value means feeding its essential components into a hash function, represented by the Hasher type. Essential components are those that contribute to the type’s implementation of Equatable. Two instances that are equal must feed the same values to Hasher in hash(into:), in the same order.


最重要的是,Hashable必须与Equatable保持一致。两件事一定不能相等,但是要有不同的哈希值。
反之则不正确。两个不相等的事物具有相同的散列是完全有效的。实际上,这是哈希的基本事实,称为pigeonhole principle。良好的哈希可以避免不必要的相等性检查,从而提高性能。但是以下hash(into:)函数始终有效:
func hash(into hasher: inout Hasher) {
    hasher.combine(0)
}
这仅意味着每个值都具有相同的哈希,因此系统将始终调用==。这对性能不利(在服务器应用程序中可能转化为拒绝服务攻击,称为哈希洪泛)。但这是合法的。
如果这是合法的,则仅对id进行哈希处理当然是合法的。
但....
这将我们带到Equatable and its docs和最重要的段落(添加了强调):

Equality implies substitutability—any two instances that compare equally can be used interchangeably in any code that depends on their values. To maintain substitutability, the == operator should take into account all visible aspects of an Equatable type. Exposing nonvalue aspects of Equatable types other than class identity is discouraged, and any that are exposed should be explicitly pointed out in documentation.


只有在任何情况下它们都可以相互替代时,才应将值视为相等,并且这不会影响程序的正确性。显然,在您的示例中,这是不正确的。实际上,对于具有可变公共(public)属性的类型,这永远都是不正确的(尽管许多教程都弄错了)。因此,您的==不正确。但是您的哈希函数很好,可以说是理想的。它的目标是快速检查是否不相等,以最大程度地减少冲突。如果id相同,则仍然必须检查其余的值,但是如果它们不同,则知道它不会相等。
如果您的Dog类型是不可变的(nameagelet而不是var),则可以以这种方式实现==。不可能手动设置id,因此不可能获得两个具有相同id但值不同的值。但是除非您可以显着提高性能,否则我不会这样做。它把正确性卡在太微妙的要求上。例如,如果扩展添加了允许直接设置initid,它将使您的==无效。 IMO太脆弱了。
私有(private)可变状态如何?只要这仅出于性能目的(内存/缓存),就可以不使用==(和哈希)。但是,如果该内部状态可以影响外部可见的行为,则它必须成为==的一部分。
好消息是,大多数时候您不必担心。 Swift的自动实现可以立即为您正确处理此问题,并比较所有属性。因此,在您的Dog示例中,最好的解决方案是仅删除方法(我确定您已经意识到;只针对正在阅读的人们说出来)。只要有可能,我强烈建议对Hashable使用默认一致性,并避免编写自己的一致性。
但是在必须自己实现的情况下,规则很简单:
  • 在所有情况下,两个相等的值必须可以完全替换,而不会影响正确性(尽管替换可能会影响性能)
  • 两个相等的值必须始终具有相同的哈希值

  • 准则也很简单:散列应该很快,同时将冲突最小化。

    对于==的这些错误实现,我看到的一个论据是试图使Set正常工作。 IMO,这是对Set和Equatable的滥用,并且不能保证以预期的方式工作(如果您插入具有相同标识符但属性不同的重复值,则不确定哪些值将在集合中)。您不应该为了使用特定的数据结构而扭曲Equatable。您应该使用符合您含义的数据结构。
    在通常情况下,正确的工具是Dictionary作为[ID: Value]。它表达了您的真正意思:一个ID与该ID的单个值之间的映射,而不是无序的唯一值包。
    使用字典而不是集合可能会消耗内存(因为您必须复制ID)。但是,只有在证明存在要解决的问题之后,您才应尝试解决该问题。

    另外,请参阅下面的亚光评论。我没有花很多时间使用新的可扩散数据源。我记得当我第一次看到他们时,我担心他们可能会滥用Equatable。如果是这样,那么您可能不得不滥用Equatable来使用它们,这将解释一些使用这种方法的教程。这并不能使其成为Swift的好工具,但是Apple框架可能需要它。

    当我对Apple的代码进行了更多的研究时(请参阅matt的答案),我注意到它们都遵循我上面讨论的规则:它们是不可变的,并且您不能在初始化期间设置UUID。这种构造使得两个值不可能具有相同的ID,而其他值却不同,因此检查ID总是足够的。但是,如果您使值可变,或者让id成为let id = UUID()以外的其他值,那么这种构造将很危险。

    关于swift - 仅考虑id来遵循Hashable是否正确?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64279211/

    相关文章:

    ios - 创建一个表示可以打开或关闭的可散列对象的协议(protocol)

    swift - 根据tableView中的附件创建条件语句

    ios - swift : Hide button in subview

    arrays - Swift 结构错误

    ios - swift 3.0 如何在 Swift 3 中访问 `AnyHashable` 中的 `Any` 类型?

    python - 为什么在该可哈希对象的集合中找不到我的可哈希对象,该集合是另一个对象的属性?

    swift - 为什么struct在转换为字典时需要符合Hashable以及Generic数组

    ios - Swift 包管理器与未版本化包有关的问题(例如 : firebase-ios-sdk)

    iphone - 如何使用 swift 更改 UICell 上字母和线条之间的间距

    python - 是什么让用户定义的类不可散列?