如果您改变其身份,HashSets不会保持元素的唯一性

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

 void Main() { HashSet testset = new HashSet(); 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 secondhashset = new HashSet(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的元素,并对事件采取行动重新检查是否相等,但这似乎是严重限制,更不用说引擎盖下的大量工作和性能损失。

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

你真的在谈论对象身份。 如果您要哈希项目,他们需要具有某种身份,以便可以进行比较。

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

这是Tester对象的问题,而不是集合。 你需要认真思考如何定义身份。 这不是一个容易的问题。

当我需要保证唯一项目的一维集合时,我通常使用Dictionary :你不能添加具有相同Key元素,而且我通常需要将一些属性附加到项目中并且Value派上用场(我的go-to值类型是许多值的Tuple<> …)。

当然,它不是最高性能,也不是最需要内存的解决方案,但我通常不会遇到性能/内存问题。

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

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