【问题标题】:Implementing -hash / -isEqual: / -isEqualTo...: for Objective-C collections实现 -hash / -isEqual: / -isEqualTo...: 用于 Objective-C 集合
【发布时间】:2010-11-09 21:18:30
【问题描述】:

注意:以下 SO 问题是相关的,但它们和链接的资源似乎都不能完全回答我的问题,特别是在为对象集合实现相等性测试方面.


背景

NSObject 提供-hash(返回实例地址,如(NSUInteger)self)和-isEqual:(返回NO,除非接收者的地址和参数相同)。这些方法被设计为在必要时被覆盖,但文档清楚地表明您应该提供两者或都不提供。此外,如果-isEqual: 为两个对象返回YES,那么这些对象的-hash 的结果必须 相同。否则,当应该相同的对象(例如 -compare: 返回 NSOrderedSame 的两个字符串实例)被添加到 Cocoa 集合或直接比较时,就会出现问题。

上下文

我开发了 CHDataStructures.framework,这是一个 Objective-C 数据结构的开源库。我已经实现了许多集合,目前正在改进和增强它们的功能。我要添加的功能之一是能够比较集合是否相等。

这些比较不应仅比较内存地址,而应考虑两个集合中存在的对象(包括排序,如果适用)。这种方式在Cocoa中有相当的先例,一般采用单独的方式,包括以下几种:

我想让我的自定义集合对相等性测试具有鲁棒性,因此它们可以安全地(并且可预测地)添加到其他集合中,并允许其他集合(如 NSSet)确定两个集合是否相等/等价/重复。

问题

-isEqualTo...: 方法本身工作得很好,但定义这些方法的类通常也会覆盖 -isEqual: 以调用 [self isEqualTo...:] 如果参数与接收者属于同一类(或可能是子类),或者 @ 987654353@ 否则。这意味着该类还必须定义 -hash 以便它为具有相同内容的不同实例返回相同的值。

此外,Apple 的 -hash 文档规定如下:(强调我的)

“如果将可变对象添加到使用哈希值确定对象在集合中的位置的集合中,则当对象在集合中时,对象的哈希方法返回的值不得更改。因此,要么散列方法不能依赖任何对象的内部状态信息或者你必须确保对象的内部状态信息在对象处于集合。因此,例如,可以将可变字典放入哈希表中,但当它在其中时,您不能更改它。(请注意,可能很难知道给定对象是否在集合中。) "

编辑: 我完全理解为什么这是必要的,并且完全同意这个推理——我在这里提到它是为了提供额外的背景,并避开了为什么会这样的话题简洁。

我的所有集合都是可变的,并且哈希必须考虑至少 一些 的内容,所以这里唯一的选择是认为改变存储在另一个集合中的集合是一个编程错误收藏。 (我的收藏都采用NSCopying,所以像NSDictionary这样的收藏可以成功复制做key等)

实现-isEqual:-hash 对我来说是有意义的,因为(例如)我的一个类的间接用户可能不知道要调用的具体-isEqualTo...: 方法,甚至不关心两个对象是否是同一类的实例。他们应该能够对id 类型的任何变量调用-isEqual:-hash 并获得预期的结果。

-isEqual:(可以访问正在比较的两个实例)不同,-hash 必须“盲目”返回结果,只能访问特定实例中的数据。 由于它无法知道哈希的用途,因此结果必须与 所有 应被视为相等/相同的可能实例一致,并且必须始终与-isEqual:一致/罢工>。 (编辑:这已经被下面的答案揭穿了,它确实让生活更轻松。)此外,编写好的哈希函数并非易事——保证唯一性是一个挑战,尤其是当你只有一个NSUInteger(32/64 位)来表示它。

问题

  1. 在为集合实施平等比较-hash时是否有最佳做法?
  2. 在 Objective-C 和 Cocoa 风格的集合中是否有任何特殊性需要规划?
  3. 是否有任何良好的单元测试方法-hash 具有合理的置信度?
  4. 关于实现-hash 以同意-isEqual: 对于包含任意类型元素的集合的任何建议?我应该知道哪些陷阱? (编辑: 没有我最初想的那么成问题——正如 @kperryua 指出的那样,“等于 -hash 值确实 暗示 -isEqual: "。)

编辑: 我应该澄清一下,我对如何为集合实现 -isEqual: 或 -isEqualTo...: 并不感到困惑,这很简单。我认为我的困惑主要源于(错误地)认为如果 -isEqual: 返回 NO,则 -hash 必须返回不同的值。过去做过密码学,我认为不同值的哈希值必须不同。然而,下面的答案让我意识到一个“好的”哈希函数实际上是关于最小化桶冲突和链接使用-hash的集合。虽然唯一的哈希值更可取,但它们并不是严格要求。

【问题讨论】:

    标签: objective-c cocoa data-structures equality chdatastructures


    【解决方案1】:

    我已经对 NSArray 和 NSMutableArray 默认哈希实现进行了一些调查,并且(除非我误解了什么)它看起来像 Apple 不遵循自己的规则:

    如果将可变对象添加到使用哈希值的集合中 确定对象在集合中的位置,返回值 通过对象的散列方法在对象被改变时不能改变 在集合中。因此,要么hash方法一定不能依赖 任何对象的内部状态信息,或者您必须确保 对象的内部状态信息不会改变,而 对象在集合中。因此,例如,一个可变字典 可以放在一个哈希表中,但你不能在它存在时更改它 那里。 (请注意,很难知道给定的 对象在集合中。)

    这是我的测试代码

    NSMutableArray* myMutableArray = [NSMutableArray arrayWithObjects:@"a", @"b", @"c", nil];
    NSMutableArray* containerForMutableArray = [NSMutableArray arrayWithObject:myMutableArray];
    
    NSUInteger hashBeforeMutation = [[containerForMutableArray objectAtIndex:0] hash];
    [[containerForMutableArray objectAtIndex:0] removeObjectAtIndex:1];
    NSUInteger hashAfterMutation = [[containerForMutableArray objectAtIndex:0] hash];
    
    NSLog(@"Hash Before: %d", hashBeforeMutation);
    NSLog(@"Hash After : %d", hashAfterMutation);
    

    输出是:

    Hash Before: 3
    Hash After : 2
    

    所以它看起来就像 NSArray 和 NSMutableArray 上 Hash 方法的默认实现是数组的计数,它不关心它是否在集合中。

    【讨论】:

    • NSMutableArray 不依赖散列来定位对象。但有趣的是,默认的哈希实现会返回计数。强化 kperryua 上面所说的内容。
    【解决方案2】:

    我认为尝试提出一些通常有用的哈希函数来为集合生成唯一的哈希值是徒劳的。 U62 将所有内容的散列组合起来的建议不会很好地扩展,因为它使散列函数 O(n)。散列函数确实应该是 O(1) 以确保良好的性能,否则散列的目的就落空了。 (考虑 plist 的常见 Cocoa 构造,它是包含数组和其他字典的字典,可能令人作呕。如果集合的哈希函数为 O( n).)

    我的建议是不要太担心集合的哈希。正如您所说,-isEqual: 意味着相等的-hash 值。另一方面,相等的-hash 值确实暗示-isEqual:。这一事实为您提供了很大的余地来创建简单的哈希。

    如果您真的担心碰撞(并且您在具体测量的实际情况中确认这是值得担心的事情),您仍然可以按照 U62 的建议一定程度上。例如,您可以获取集合中第一个和/或最后一个元素的哈希值,并将其与集合的-count 组合。这足以提供一个不错的哈希值。

    我希望至少能回答您的一个问题。

    至于第 1 点:实施 -isEqual: 非常简单。您枚举内容,并检查每个元素上的 isEqual:。

    需要注意的一件事可能会影响您决定为集合的-hash 函数执行的操作。您收藏的客户还必须了解管理-isEqual:-hash 的规则。如果您在收藏的-hash 中使用内容的-hash,如果内容的isEqual:-hash 不同意,您的收藏就会中断。当然,这是客户的错,但这是另一个反对将您的 -hash 与集合内容无关的论点。

    没有。 2有点模糊。不确定您的想法。

    【讨论】:

    • 好答案。 “相等的 -hash 值不暗示 -isEqual:”这一事实极大地简化了问题的范围并改变了我思考它的方式。我想知道在对象中缓存哈希值是否有意义,并在添加或删除对象时更新它。这分摊了计算哈希的成本,并且仅在修改集合时更新它,而不是每次调用 -hash 时。对于散列本身,实现一致分布当然是理想的,但它是与速度的平衡。我现在将尝试计数、第一个和最后一个(如果 > 1)的 XOR。 :-)
    • 关于我的第二个问题,我想我关心的是匹配 Foundation 集合实现 isEqual: 和哈希的方式。由于 Core Foundation 集合的 Objective-C 对应物是封闭源代码,因此我无法知道我是否正在使用“可可方式”。我想模仿现有集合的行为以遵守“最小惊喜原则”。此外,Cocoa 中的平等契约与 Java 等语言略有不同。我一直在阅读这篇文章的想法:karlkraft.com/index.php/2008/01/07/equality-vs-identity
    【解决方案3】:

    如果两个集合包含相同的元素,则它们应该被认为是相等的,并且如果集合是有序的,则元素的顺序相同。

    关于集合的散列,以某种方式组合元素的散列就足够了(异或或模相加)。请注意,虽然规则规定根据 IsEqual 相等的两个对象需要返回相同的散列,但相反的情况并不成立:尽管散列的唯一性是可取的,但对于解决方案的正确性来说,这不是必需的。因此有序集合不需要考虑元素的顺序。

    顺便说一句,Apple 文档的摘录是一个必要的限制。一个对象不能在突变下保持相同的哈希值,同时还要确保具有相同值的对象具有相同的哈希值。这适用于最简单的对象和集合。当然,只有当对象位于使用散列组织其元素的容器内时,对象的散列值才会发生变化。所有这一切的结果是,可变集合在放置在另一个容器中时不应该发生变异,但是任何具有真正哈希函数的对象也不应该发生变异。

    【讨论】:

    • 我已经编辑了我的问题,以澄清我对您的第一段和最后一段的先前理解。感谢您指出 -hash 不是 必需 为不同的值返回不同的值。起初它没有意义,但对使用散列的集合的快速复习让我想起了桶和链。您对创建哈希的建议是好的,但@kperryua 提出了一个关于哈希可扩展性的重要观点,因为我在谈论集合。
    猜你喜欢
    • 2012-12-29
    • 1970-01-01
    • 2011-10-29
    • 2010-09-20
    • 1970-01-01
    • 1970-01-01
    • 2023-03-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多