仅考虑 id 来符合 Hashable 是否正确?

Che*_*eng 9 hashable swift

我遇到了很多在线示例,当他们尝试符合时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
  }
}
Run Code Online (Sandbox Code Playgroud)

我想知道,这是符合的正确方法Hashable吗?我认为我们应该考虑所有类成员变量?

例如,仅idfunc 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)")
Run Code Online (Sandbox Code Playgroud)

我想知道,Hashable仅通过id考虑来符合是否正确?


每秒

我尝试从 Java 等其他语言中了解有关哈希代码生成的一般建议。这就是他们流行的 Effective Java 书中所写的内容。

不要试图从哈希码计算中排除重要字段以提高性能。虽然生成的散列函数可能运行得更快,但其较差的质量可能会降低散列表的性能,使其无法使用。特别是,散列函数可能会遇到大量实例,这些实例主要在您选择忽略的区域中有所不同。如果发生这种情况,散列函数会将所有这些实例映射到几个散列代码,而应该在线性时间内运行的程序将在二次时间内运行。这不仅仅是一个理论问题。在 Java 2 之前,String 散列函数使用最多 16 个字符均匀分布在整个字符串中,从第一个字符开始。对于分层名称的大型集合,例如 URL,

Rob*_*ier 5

TL;DR:这个散列函数是不必要的,但合法的,可以说是理想的。尽管在教程中很常见,但这个 == 是不正确的,因为它破坏了 Equatable 所要求的可替代性,正如您所建议的那样。

然而,正如 matt 所指出的,diffable 数据源无论如何可能需要这样做。这并没有使它好,但它可能使它成为必要。(请阅读下面所有 matt 的评论。它们提供了许多重要的上下文。特别是关于 diffable 数据源的参考,请参阅他的回答;我对 diffable 数据源不是特别熟悉。)


我建议转向文档,其中列出了这一点。

首先,可哈希

散列一个值意味着将其基本组成部分提供给一个散列函数,由 Hasher 类型表示。基本组件是那些有助于 Equatable 类型实现的组件。两个相等的实例必须以hash(into:)相同的顺序将相同的值提供给 中的 Hasher 。

最重要的是Hashable与Equatable保持一致。两件事永远不能相等,但具有不同的哈希值。

反过来说是不对的。两个不相等的事物具有相同的哈希值是完全有效的。事实上,这是散列的一个基本事实,称为鸽巢原理。一个好的散列通过避免不必要的相等检查来提高性能。但以下hash(into:)函数始终有效:

func hash(into hasher: inout Hasher) {
    hasher.combine(0)
}
Run Code Online (Sandbox Code Playgroud)

这只是意味着每个值都具有相同的哈希值,因此系统将始终调用 ==。这对性能不利(并且在服务器应用程序中可以转化为称为哈希泛洪的拒绝服务攻击)。但这是合法的。

如果这是合法的,当然只是散列id是合法的。

但....

这将我们带到了Equatable 及其文档,以及最重要的段落(强调):

相等意味着可替代性——任何两个相等比较的实例都可以在依赖于它们的值的任何代码中互换使用。为了保持可替换性,== 运算符应考虑 Equatable 类型的所有可见方面。不鼓励公开 Equatable 类型的非值方面而不是类标识,并且应该在文档中明确指出任何公开的方面。

一个值只有在任何情况下都可以相互替换时才被认为是相等的,并且不会影响程序的正确性。很明显,在你的例子中,这不是真的。事实上,对于具有可变公共属性的类型来说永远不会是真的(尽管很多教程都弄错了)。所以你的 == 是不正确的。但是您的哈希函数很好,可以说是理想的。它的目标是快速检查不等式,从而最大限度地减少冲突。如果 id 相同,您仍然需要检查其余的值,但如果它们不同,您就知道它们不会相等。

如果你的狗型是不变的(name并且agelet不是var),这可能是实现==这样可以接受的。id手动设置是不可能的,因此不可能获得具有相同id但不同值的两个值。但我不会这样做,除非你能表现出显着的性能提升。它把正确性挂在太微妙的要求上。例如,如果扩展程序直接添加了init允许设置id,则会使您的 == 无效。IMO 太脆弱了。

私有可变状态怎么样?只要这仅用于性能目的(记忆/缓存),那么忽略 ==(和哈希)就可以了。但是,如果该内部状态可以影响外部可见的行为,那么它就需要成为 == 的一部分。

好消息是,大多数时候您无需担心。Swift 的自动实现开箱即用地为您正确处理此问题,并比较所有属性。因此,在您的 Dog 示例中,最好的解决方案是删除这些方法(我确定您知道这一点;只是将其说明给大家阅读)。只要有可能,我强烈建议使用 Hashable 的默认一致性并避免编写自己的一致性。

但在你必须实现自己的情况下,规则很简单:

  • 在不影响正确性的情况下,两个相等的值必须完全可替换(尽管替换可能会影响性能)
  • 两个相等的值必须始终具有相同的哈希值

指导方针也相当简单:散列应该很快,同时尽量减少冲突。


对于 == 的这些不正确的实现,我看到的一个论点是尝试使Set工作很好地工作。IMO,这是对 Set 和 Equatable 的滥用,并且不承诺以预期的方式工作(如果您插入具有相同标识符但不同属性的重复值,则未定义哪些值将在集合中)。您不应该为了使用特定的数据结构而扭曲 Equatable。您应该使用与您的意思相匹配的数据结构。

在常见情况下,正确的工具是 Dictionary as [ID: Value]。它表达了您真正的意思:一个 ID 和该 ID 的单个值之间的映射,而不是一个无序的唯一值袋。

使用 Dictionary 而不是 Set 可能会消耗内存(因为您必须复制 ID)。但是,您应该仅在证明存在需要解决的问题后尝试解决该问题。


另外,请参阅下面的马特评论。我没有花很多时间在新的可区分数据源上。我记得当我第一次看到他们时,我担心他们可能会滥用 Equatable。如果这是真的,那么您可能不得不滥用 Equatable 来使用它们,这将解释一些以这种方式执行的教程。这并不使它成为好的 Swift,但它可能是 Apple 框架所必需的。


由于我对 Apple 的代码进行了更多研究(请参阅 matt 的许多答案),我注意到它们都遵循我上面讨论的规则:它们是不可变的,并且您不能在 init 期间设置 UUID。这种结构使得两个值不可能具有相同的 id 但其他值不同,因此检查 id 总是足够的。但是,如果您使值可变,或者您允许 id 是 id 以外的任何内容let id = UUID(),那么这种构造就会变得危险。

  • 好吧,公平地说,OP 指出的第一个教程中的所有内容都是错误的,你可能会晕倒。 (2认同)

Dáv*_*tor 2

仅使用属性子集来实现一致性是否正确Hashable完全取决于您的要求。

如果对于某个对象,相等性实际上仅由单个变量(或变量子集)定义,那么使用该变量子集Hashable(和Equatable一致性)是正确的。

但是,如果需要类型的所有属性来确定两个实例是否相等,那么您应该使用所有属性。