【问题标题】:how to force HashSet to rehash members?如何强制 HashSet 重新散列成员?
【发布时间】:2018-06-08 17:24:16
【问题描述】:

在这种情况下,一个成员被编辑为与另一个成员相等,强制 HashSet 重新计算哈希从而清除重复项的正确方法是什么?

我知道不要期望这会自动发生,所以我尝试了诸如将 HashSet 与自身相交,然后将其重新分配给引用自身和相同 EqualityComparer 的构造函数调用。我认为后者肯定会起作用,但没有。

确实成功的一件事是将 HashSet 从其转换为其他容器类型(如 List)重构,而不是直接从其自身重构。

类定义:

public class Test {
    public int N;
    public override string ToString() { return this.N.ToString(); }
    }
public class TestClassEquality: IEqualityComparer<Test> {
    public bool Equals(Test x, Test y) { return x.N == y.N; }
    public int GetHashCode(Test obj) { return obj.N.GetHashCode(); }
    }

测试代码:

    TestClassEquality eq = new TestClassEquality();
    HashSet<Test> hs = new HashSet<Test>(eq);
    Test a = new Test { N = 1 }, b = new Test { N = 2 };
    hs.Add(a);
    hs.Add(b);
    b.N = 1;
    string fmt = "Count = {0}; Values = {1}";
    Console.WriteLine(fmt, hs.Count, string.Join(",", hs));
    hs.IntersectWith(hs);
    Console.WriteLine(fmt, hs.Count, string.Join(",", hs));
    hs = new HashSet<Test>(hs, eq);
    Console.WriteLine(fmt, hs.Count, string.Join(",", hs));
    hs = new HashSet<Test>(new List<Test>(hs), eq);
    Console.WriteLine(fmt, hs.Count, string.Join(",", hs));

输出:

"Count: 2; Values: 1,1"
"Count: 2; Values: 1,1"
"Count: 2; Values: 1,1"
"Count: 1; Values: 1"

基于最终方法的成功,我可能会创建一个扩展方法,其中 HashSet 将自身转储到本地列表中,清除自身,然后从所述列表中重新填充。

这真的有必要吗,还是有一些更简单的方法可以做到这一点?

【问题讨论】:

  • 这里的问题是你正在做一些明确不应该发生的事情,用于散列和字典的键必须不改变。因此,没有人可以轻松处理这种情况。

标签: c# hashset iequalitycomparer


【解决方案1】:

Lasse 的评论是正确的:HashSet 的合同要求你不要这样做,所以当你这样做时问该怎么做是行不通的。如果您这样做时感到疼痛,停止这样做如果突变会导致其哈希值在集合中发生变化,则不能将可变对象放入哈希集中。你是自己造成的裂痕。

要摆脱那根裂棍,你可以:

  • 当对象位于哈希集中时,停止对其进行变异。在变异之前移除它们,稍后再将它们放回去。
  • 修复对象上相等和散列的实现,使其在突变之间保持一致。
  • 创建散列集时,提供自定义散列/相等算法,该算法在对象发生变异时不会改变其观点。
  • 实现您自己的“set”类,该类在此方案中具有您喜欢的任何行为。这是非常困难的,所以要小心。 (首先创建此限制是有原因的!)

【讨论】:

  • 谢谢。 TLDR 非 MVCE 版本是我最初有一个 Dictionary ,其中键字符串也是 Foo 的“名称”属性——但后来意识到这是多余和复杂的,不可预见的需要从调用环境。因此,我根据所述名称切换到具有相等性的 HashSet 并遇到了上述问题。当前版本使用带有私有 List 和访问器函数的包装类,用于所需的按名称查找、重命名和避免重复。预计计数仍然太低,线性查找效率不会成为问题。
【解决方案2】:

除了重新创建HashSet&lt;&gt;,别无他法。可悲的是,HashSet&lt;&gt; 构造函数有一个优化,因此如果它是从另一个 HashSet&lt;&gt; 创建的,它会复制哈希码......所以我们可以作弊:

hs = new HashSet<Test>(hs.Skip(0), eq);

hs.Skip(0)IEnumerable&lt;&gt;,而不是 HashSet&lt;&gt;。这会破坏HashSet&lt;&gt; 检查。

请注意,不能保证Skip() 将来不会在为 0 的情况下实现短路,例如:

if (count == 0)
{
    return enu;
}
else
{
    return count elements;
}

(见 Lippert 的评论,错误问题)

“手动”的方法是:

var hs2 = new HashSet<Test>(eq);
foreach (var value in hs)
{
    hs2.Add(value);
}
hs = hs2;

所以“手动”枚举并读取。

【讨论】:

  • 我刚刚发现的另一件事是,使用同一 EqualityComparer 类的不同实例可以避免这种优化。 hs = new HashSet&lt;Test&gt;(hs, eq2);
  • @CusterBarnes 是的。见HashSet&lt;&gt;的来源this
  • 我喜欢您仔细考虑这一点,但您实际上不必担心。 Skip(0)Select(x=&gt;x) 等的标准实现要求不等于底层集合的引用,因此您不必担心它们会改变。 LINQ 的设计使您不能“抛弃”查询以返回原始对象;开发者可能试图向消费者隐藏一个底层的可变对象,这需要一个不可变的集合视图。
【解决方案3】:

如您所见,HashSets 不处理可变对象,因为修改对象会影响其哈希码或与其他对象的相等性。只需将其删除并重新添加即可:

hs.Remove(b);
b.N = 1;
hs.Add(b);

【讨论】:

  • 请注意,您必须在 突变之前进行删除,就像您在此处所做的那样。如果对象发生了变异,使得哈希值不同,那么你不能删除它。这就是为什么这是非法的全部原因!
猜你喜欢
  • 1970-01-01
  • 2012-11-07
  • 1970-01-01
  • 2017-06-13
  • 2011-09-19
  • 2019-08-25
  • 2013-10-13
  • 2012-11-23
  • 2011-07-03
相关资源
最近更新 更多