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

时间:2020-10-09 11:31:57

标签: 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书籍中写的。

不要试图从哈希码中排除重要字段 计算以提高性能。而产生的哈希函数 可能运行速度更快,其质量较差可能会降低哈希表的性能 直至无法使用。特别是哈希 函数可能会遇到大量实例, 主要区别在于您选择忽略的区域。如果发生这种情况, 哈希函数会将所有这些实例映射到一些哈希代码,并且 应该在线性时间内运行的程序将改为二次运行 时间。这不仅仅是一个理论问题。在Java 2之前, 字符串散列函数,最多使用16个字符,这些字符均匀分布 整个字符串中,从第一个字符开始。对于大 层次结构名称的集合,例如URL,此功能 完全显示了先前描述的病理行为。

6 个答案:

答案 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类型是不可变的(nameagelet而不是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
  }
}
相关问题