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

Swift 具有可互换属性的哈希结构?,swift,hashable,Swift,Hashable,我需要使一个自定义结构符合Hashable,以便将其用作字典键类型。然而,挑战在于,为了识别唯一实例,结构的两个属性是可互换的 下面是一个简化的示例来说明问题: struct MultiplicationQuestion { let leftOperand: Int let rightOperand: Int var answer: Int { return leftOperand * rightOperand } } 识别唯一的乘法问题的两个重要属性是左操作数和右操作

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

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

struct MultiplicationQuestion {
    let leftOperand: Int
    let rightOperand: Int
    var answer: Int { return leftOperand * rightOperand }
}
识别唯一的
乘法问题
的两个重要属性是
左操作数
右操作数
,但它们的顺序并不重要,因为“1 x 2”与“2 x 1”本质上是同一个问题。(由于一些原因,我不想在这里赘述,它们需要作为单独的财产保存。)

我尝试如下定义
可散列
一致性,因为我知道我为
==
定义的相等性与内置散列器将要执行的操作之间存在冲突:

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)
var oneTimesTables=Set()
var twoTimesTables=Set()
因为我在1…5{
插入(乘法问题(左操作数:1,右操作数:i))
插入(乘法问题(左操作数:2,右操作数:i))
}
让commonQuestions=oneTimesTables.intersection(两个时间表)
让allQuestions=oneTimesTables.union(两个时间表)
希望的结果(一厢情愿)是
commonQuestions
包含一个问题(1 x 2),而
allQuestions
包含九个问题,删除了重复的问题

然而,实际结果是不可预测的。如果我在操场上跑多次,我会得到不同的结果。大多数情况下,
commonQuestions.count
为0,但有时为1。大多数时候,
allQuestions.count
是10,但有时是9。(我不确定我期望的是什么,但这种不一致肯定是一个惊喜!)


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

这就是Hasher的工作方式

然而,底层哈希算法的设计目的是展示 雪崩效应:对种子或输入字节的轻微更改 序列通常会在生成的哈希中产生剧烈的变化 价值观

这里的问题是hash(into:)func

由于顺序很重要,
合并
操作是不可交换的。您应该找到一些其他函数作为此结构的哈希。在你的情况下,最好的选择是

    func hash(into hasher: inout Hasher) {
        hasher.combine(leftOperand & rightOperand)
    }
正如@Martin R所指出的,碰撞较少,最好使用
^

    func hash(into hasher: inout Hasher) {
        hasher.combine(leftOperand ^ rightOperand)
    }
(和评论)对我帮助很大,我已经将其标记为正确。尽管如此,我认为值得添加另一个答案来分享我学到的一些东西,并提出另一种解决问题的方法

苹果说:

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

如果这是一个简单的一对一的属性比较(就像所有代码示例所示!),那就很好了,但是如果你的
==
方法有我这样的条件逻辑呢?如何将其转换为一个(或多个)值以提供给哈希器

我一直在关注这个细节,直到Tiran建议给散列程序提供一个常量值(如2)仍然有效,因为散列冲突是通过
=
解决的。当然,您不会在生产环境中这样做,因为您将失去哈希查找的所有性能优势,但我得到的教训是,如果您不能使哈希器参数与
=
操作数完全相同,请使哈希器相等逻辑更具包容性(而不是更少)

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

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

uniqueOperands = Set([leftOperand, rightOperand])
一个排序的数组也会起作用,但是一个集合似乎是更优雅的选择。由于集合没有排序,因此我的
==
(使用
&&
|
)的详细条件逻辑已经整齐地封装在
集合
类型中

现在,我可以使用完全相同的值来测试相等性并为哈希器提供数据:

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

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

我已经测试了性能,它与按位操作是一致的。不仅如此,我的代码在这个过程中变得更加简洁易读。似乎是双赢。

谢谢@Tiran。不幸的是,
答案
无法始终识别唯一的问题…2 x 5和1 x 10都会给出相同的答案,但它们是不同的问题<代码>左操作数和右操作数可能更有希望!二进制操作会让我头疼,但我会看一看。(在等待答案的同时,我还想知道为什么我没有尝试将操作数组合到一个集合或排序数组中。我也将要测试这个想法。)或者
hasher.combine(leftOperand^rightOperand)
,以稍微减少冲突。@Kal:哈希值不需要是(通常不能是)唯一。假设您有
f
,这是一些散列函数。这意味着
f(a)!=f(b)=>a!=b
。但是
f(a)==f(b)
并不意味着
a==b
f(a)=f(b)
a!=b
称为碰撞。函数的冲突次数不应影响对象之间的比较结果,它将