【问题标题】:HashSets don't keep the elements unique if you mutate their identity如果你改变它们的身份,HashSets 不会保持元素的唯一性
【发布时间】:2012-07-09 19:01:17
【问题描述】:

在 C# 中使用 HashSets 时,我最近遇到了一个烦人的问题:HashSets 不保证元素的唯一性;它们不是集合。他们所做的保证是,当Add(T item) 被调用时,如果集合中的任何项目item.equals(that)true,则不会添加该项目。如果您操作集合中已有的项目,这将不再成立。一个演示的小程序(来自我的 Linqpad 的 copypasta):

void Main()
{
    HashSet<Tester> testset = new HashSet<Tester>();
    testset.Add(new Tester(1));
    testset.Add(new Tester(2));
    foreach(Tester tester in testset){
      tester.Dump();
    }
    foreach(Tester tester in testset){
      tester.myint = 3;
    }
    foreach(Tester tester in testset){
      tester.Dump();
    }
    HashSet<Tester> secondhashset = new HashSet<Tester>(testset);
    foreach(Tester tester in secondhashset){
      tester.Dump();
    }
}

class Tester{
  public int myint;

  public Tester(int i){
    this.myint = i;
  }

  public override bool Equals(object o){
    if (o== null) return false;
    Tester that = o as Tester;
    if (that == null) return false;
    return (this.myint == that.myint);
  }

  public override int GetHashCode(){
    return this.myint;
  }

  public override string ToString(){
    return this.myint.ToString();
  }
}

它会很高兴地将集合中的项目操作为相等,仅在构建新的 HashSet 时将它们过滤掉。当我想使用需要知道条目是唯一的集合时,有什么建议?滚动我自己的,其中 Add(T item) 添加项目的副本,而枚举器枚举所包含项目的副本?这带来了挑战,即每个包含的元素都应该是可深度复制的,至少在影响其平等的项目中是这样。

另一种解决方案是自行推出,只接受实现 INotifyPropertyChanged 的​​元素,并对事件采取行动以重新检查是否相等,但这似乎严重限制,更不用说大量工作和性能损失在引擎盖下。

我想到的另一个可能的解决方案是确保构造函数中的所有字段都是只读的或 const 的。所有解决方案似乎都有很大的缺点。我还有其他选择吗?

【问题讨论】:

  • 我不确定我是否理解这个问题...您想知道哪种集合可以保证其中没有两个项目是相等的?
  • 蒂姆,确实如此,您可能需要向下滚动
  • @Martijn:我忽略了GetHashCode。无论如何,他应该阅读 Eric Lippert 的这篇博客:blogs.msdn.com/b/ericlippert/archive/2011/02/28/… 它解释了它的规则和指导方针。例如:“准则:GetHashCode 返回的整数永远不应该改变”“规则:当对象包含在依赖于哈希码保持稳定”
  • @Tim,谢谢!这几乎就是事情出错的地方。这也意味着一旦实例化并添加到集合中,对象就永远不能改变身份。真正的问题不在于哈希码,而在于等号。我可以将其更改为 public int GetHashCode() { return 0 } 并且哈希码将保持不变,但问题仍然存在。
  • 您涉及INotifyPropertyChanged 的“自己动手”解决方案指出了这里的真正问题:当其中一个元素突变为与另一个元素“相同”时,该集合应该怎么做?扔一个?哪一个?您需要先定义语义,然后才能寻找解决方案。

标签: c# .net clr hashset


【解决方案1】:

您实际上是在谈论对象身份。如果您要对项目进行哈希处理,它们需要具有某种身份,以便可以进行比较。

  • 如果发生变化,则它不是有效的身份验证方法。您目前拥有public int myint。确实应该是readonly,并且只能在构造函数中设置。
  • 如果两个对象在概念上不同(即您希望在特定设计中将它们视为不同),那么它们的哈希码应该不同。
  • 如果您有两个具有相同内容的对象(即两个具有相同字段值的值对象),那么它们应该具有相同的哈希码并且应该相等。
  • 如果您的数据模型表明您可以拥有两个具有相同内容的对象,但它们不能相等,那么您应该使用代理 ID,而不是对内容进行哈希处理。
  • 也许你的对象应该是不可变的值类型,所以对象不能改变
  • 如果它们是可变类型,则应为给定对象分配一个永远不会更改的代理 ID(即从外部引入的代理 ID,例如递增的计数器 ID 或使用对象的哈希码)

这是您的 Tester 对象的问题,而不是集合的问题。您需要认真考虑如何定义身份。这不是一个简单的问题。

【讨论】:

  • 所以你会采取某种选项 3:只使用具有不可变身份的 HashSet?你怎么保证?
  • “如果您有多个具有给定标识的对象,则它不是有效的标识方法”- 是的(我认为),但我一开始误读了这一点:如果您这样做会有所帮助可以用一种不会向粗心的读者暗示两个不同的对象必须从 GetHashCode 返回不同的哈希码的方式重新表述这一点——这当然是完全有效的。
  • “如果两个对象在概念上是不同的(即您想在您的特定设计中将它们视为不同),那么它们的哈希码应该不同。”?不,这就是我希望我一开始误读的方式,但那是错误的。两个不同的对象可以具有相同的哈希码并且不相等。 HashSet 对此没有任何问题。举个简单的例子,只有 2**32 个可能的哈希码,您是否希望 Int64 的所有有效值的所有哈希码都不同?
  • 结论是,如果你的价值观发生了变化,那么你就不能用它们来定义身份。有强有力的指导方针和大量的框架,但它们不能阻止你在脚下开枪。有些人有有效的用例来射击自己的脚。
【解决方案2】:

当我需要保证唯一项目的一维集合时,我通常使用Dictionary&lt;TKey, Tvalue&gt;:您不能添加具有相同Key 的元素,另外我通常需要将一些属性附加到项目和Value派上用场(对于许多值,我的首选值类型是 Tuple&lt;&gt;...)。

当然,它不是性能最高的解决方案,也不是最不占用内存的解决方案,但我通常不会担心性能/内存问题。

【讨论】:

    【解决方案3】:

    您应该实现自己的 IEqualityComparer 并将其传递给 HashSet 的构造函数,以确保获得所需的相等比较器。

    正如 Joe 所说,如果您希望集合在 .Add(T item) 之外仍然保持唯一性,您需要使用由构造函数创建且没有公开可见的集合属性的 ValueObjects。 即

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-06-06
      • 2013-07-28
      • 2011-01-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多