具有可互换属性的哈希结构?

时间:2019-07-09 04:26:22

标签: swift hashable

我需要使自定义结构符合Hashable,以便可以将其用作Dictionary键类型。但是,挑战在于,为了识别唯一实例,该结构的两个属性可以互换。

下面是一个简化的示例来说明问题:

struct MultiplicationQuestion {
    let leftOperand: Int
    let rightOperand: Int
    var answer: Int { return leftOperand * rightOperand }
}

用于标识唯一MultiplicationQuestion的两个重要属性是leftOperandrightOperand,但它们的顺序无关紧要,因为'1 x 2'本质上是与“ 2 x 1”相同的问题。 (出于我不会在这里讨论的原因,它们需要保留为单独的属性。)

我尝试如下定义Hashable一致性,因为知道在为==定义的相等性与内置的Hasher要做的事情之间存在冲突:

extension MultiplicationQuestion: Hashable {
    static func == (lhs: MultiplicationQuestion, rhs: MultiplicationQuestion) -> Bool {
        return (lhs.leftOperand == rhs.leftOperand && lhs.rightOperand == rhs.rightOperand) || (lhs.leftOperand == rhs.rightOperand && lhs.rightOperand == rhs.leftOperand)
    }
    func hash(into hasher: inout Hasher) {
        hasher.combine(leftOperand)
        hasher.combine(rightOperand)
    }
}

我通过创建两个问题集并对它们执行各种操作来对此进行测试:

var oneTimesTables = Set<MultiplicationQuestion>()
var twoTimesTables = Set<MultiplicationQuestion>()
for i in 1...5 {
    oneTimesTables.insert( MultiplicationQuestion(leftOperand: 1, rightOperand: i) )
    twoTimesTables.insert( MultiplicationQuestion(leftOperand: 2, rightOperand: i) )
}

let commonQuestions = oneTimesTables.intersection(twoTimesTables)
let allQuestions = oneTimesTables.union(twoTimesTables)

希望的结果(如意算盘)是commonQuestions包含一个问题(1 x 2),而allQuestions包含9个问题,已删除了重复项。

实际 结果是不可预测的。如果我多次运行游乐场,则会得到不同的结果。在大多数情况下,commonQuestions.count为0,但有时为1。在大多数情况下,allQuestions.count为10,但有时为9。(我不确定自己在期待什么,但是这种不一致肯定是一个惊喜!)

如何使hash(into:)方法为两个属性相同但取反的实例生成相同的哈希值?

2 个答案:

答案 0 :(得分:2)

这是Hasher的工作方式

https://developer.apple.com/documentation/swift/hasher

  

但是,底层哈希算法旨在展示   雪崩效应:种子或输入字节略有变化   序列通常会在生成的哈希中产生剧烈变化   值。

所以这里的问题在hash(into :) func

由于顺序很重要combine操作是不可交换的。您应该找到其他函数作为该结构的哈希。您的最佳选择是

    func hash(into hasher: inout Hasher) {
        hasher.combine(leftOperand & rightOperand)
    }

正如@Martin R指出的那样,冲突较少,最好使用^

    func hash(into hasher: inout Hasher) {
        hasher.combine(leftOperand ^ rightOperand)
    }

答案 1 :(得分:0)

Tiran Ut's answer(和评论)对我有很大帮助,我将其标记为正确。尽管如此,我认为还是有必要添加另一个答案,以分享我学到的一些知识并提出解决问题的另一种方法。

Apple的hash(into:) documentation说:

  

用于散列的组件必须与组件相同   在您类型的==运算符实现中进行了比较。

如果对属性进行简单的一对一比较(如所有代码示例所示!),那很好,但是如果您的==方法具有像我这样的条件逻辑怎么办?您如何将其转换为一个或多个值以填充哈希器?

我一直迷上这个细节,直到Tiran建议仍然为哈希器提供一个恒定值(如2)仍然可行,因为无论如何==可以解决哈希冲突。当然,您不会在生产环境中这样做,因为您会失去哈希查找的所有性能优势,但对我而言,要是不能正确地输入哈希参数 与您的==操作数相同,使哈希器相等逻辑更多包含(不小于)。

Tiran Ut的答案之所以起作用,是因为按位运算并不关心操作数的顺序,就像我的==逻辑一样。有时候,两个完全不同的对可能会产生相同的值(导致有保证的哈希冲突),但是在这种情况下,唯一真正的结果是对性能的影响很小。

最后,我意识到,我可以在两种情况下使用完全相同的逻辑,从而避免了哈希冲突-除了由不完善的哈希算法引起的任何冲突之外。我向MultiplicationQuestion添加了一个新的私有常量,并将其初始化如下:

uniqueOperands = Set([leftOperand, rightOperand])

排序数组也可以,但是Set似乎是更优雅的选择。由于Set没有排序,因此我对==(使用&&||)的详细条件逻辑已经很好地封装在Set类型中。

现在,我可以使用相同的值来测试是否相等并填充哈希器:

static func ==(lhs: MultiplicationQuestion, rhs: MultiplicationQuestion) -> Bool {
    return lhs.uniqueOperands == rhs.uniqueOperands
}

func hash(into hasher: inout Hasher) {
    hasher.combine(uniqueOperands)
}

我已经测试了性能,它与按位运算相当。不仅如此,我的代码在此过程中也变得更加简洁易读。似乎是双赢的。