HashSet 如何比较元素是不是相等?

Posted

技术标签:

【中文标题】HashSet 如何比较元素是不是相等?【英文标题】:How does HashSet compare elements for equality?HashSet 如何比较元素是否相等? 【发布时间】:2012-02-15 15:27:03 【问题描述】:

我有一个班级是IComparable:

public class a : IComparable

    public int Id  get; set; 
    public string Name  get; set; 

    public a(int id)
    
        this.Id = id;
    

    public int CompareTo(object obj)
    
        return this.Id.CompareTo(((a)obj).Id);
    

当我将此类的对象列表添加到哈希集时:

a a1 = new a(1);
a a2 = new a(2);
HashSet<a> ha = new HashSet<a>();
ha.add(a1);
ha.add(a2);
ha.add(a1);

一切都很好,ha.count2,但是:

a a1 = new a(1);
a a2 = new a(2);
HashSet<a> ha = new HashSet<a>();
ha.add(a1);
ha.add(a2);
ha.add(new a(1));

现在ha.count3

    为什么HashSet 不尊重aCompareTo 方法。 HashSet 是拥有唯一对象列表的最佳方式吗?

【问题讨论】:

在构造函数中添加IEqualityComparer&lt;T&gt;的实现或者在a类中实现。 msdn.microsoft.com/en-us/library/bb301504(v=vs.110).aspx 【参考方案1】:

它使用IEqualityComparer&lt;T&gt;EqualityComparer&lt;T&gt;.Default,除非您在构造时指定不同的)。

当你将一个元素添加到集合中时,它会使用IEqualityComparer&lt;T&gt;.GetHashCode找到哈希码,并将哈希码和元素都存储起来(当然是在检查元素是否已经在集合中之后)。

要查找一个元素,它将首先使用IEqualityComparer&lt;T&gt;.GetHashCode 来查找哈希码,然后对于具有相同哈希码的所有元素,它将使用IEqualityComparer&lt;T&gt;.Equals 来比较实际是否相等。

这意味着您有两个选择:

将自定义IEqualityComparer&lt;T&gt; 传递给构造函数。如果您不能修改 T 本身,或者您想要一个非默认的相等关系(例如“所有具有负用户 ID 的用户都被认为是相等的”),这是最好的选择。这几乎从未在类型本身上实现(即Foo 没有实现IEqualityComparer&lt;Foo&gt;),而是在一个单独的类型中实现,仅用于比较。 通过覆盖GetHashCodeEquals(object) 在类型本身中实现相等。理想情况下,也要在类型中实现IEquatable&lt;T&gt;,尤其是在它是值类型的情况下。这些方法将由默认的相等比较器调用。

请注意,就 ordered 比较而言,这一切都不是 - 这是有道理的,因为在某些情况下,您可以轻松地指定相等但不能指定全序。这和Dictionary&lt;TKey, TValue&gt;基本一样。

如果您想要一个使用 ordering 而不仅仅是相等比较的集合,您应该使用 .NET 4 中的 SortedSet&lt;T&gt; - 它允许您指定 IComparer&lt;T&gt; 而不是 IEqualityComparer&lt;T&gt; .这将使用IComparer&lt;T&gt;.Compare - 如果您使用的是Comparer&lt;T&gt;.Default,它将委托给IComparable&lt;T&gt;.CompareToIComparable.CompareTo

【讨论】:

+1 另请注意@tyriker 的回答(IMO 应在此处发表评论)指出利用IEqualityComparer&lt;T&gt;.GetHashCode/Equals() 的最简单方法是在@987654347 上实施EqualsGetHashCode @ 本身(当你这样做的时候,你也会实现强类型的对应物:-bool IEquatable&lt;T&gt;.Equals(T other) 虽然这个答案非常准确,但可能有点令人困惑,尤其是对于新用户来说,因为它没有明确说明对于最简单的情况覆盖 EqualsGetHashCode 就足够了 - 正如@tyriker's 中所述回答。 Imo 一旦你实现了IComparable(或IComparer),你不应该被要求单独实现相等(而只是GetHashCode)。从某种意义上说,可比接口应该继承自相等接口。我确实理解拥有两个独立函数的性能优势(您可以通过说出某事物是否相等来分别优化相等性)但仍然......否则当您在CompareTo 函数中指定实例何时相等时非常令人困惑,并且框架不会考虑这一点。 @nawfal 并非所有事物都有逻辑顺序。如果您要比较两个包含 bool 属性的东西,那么必须编写类似 a.boolProp == b.boolProp ? 1 : 0 或者应该是 a.boolProp == b.boolProp ? 0 : -1a.boolProp == b.boolProp ? 1 : -1 之类的东西真是太糟糕了。哎呀! @Simon_Weaver 是。我确实想在我提出的假设特征中以某种方式避免它。【参考方案2】:

这里澄清了部分未说明的答案:HashSet&lt;T&gt; 的对象类型不必实现IEqualityComparer&lt;T&gt;,而只需覆盖Object.GetHashCode()Object.Equals(Object obj)

而不是这个:

public class a : IEqualityComparer<a>

  public int GetHashCode(a obj)  /* Implementation */ 
  public bool Equals(a obj1, a obj2)  /* Implementation */ 

你这样做:

public class a

  public override int GetHashCode()  /* Implementation */ 
  public override bool Equals(object obj)  /* Implementation */ 

这很微妙,但这让我在一天的大部分时间里试图让 HashSet 按预期方式运行。就像其他人所说的那样,HashSet&lt;a&gt; 最终会在使用该集合时根据需要调用a.GetHashCode()a.Equals(obj)

【讨论】:

好点。顺便说一句,正如我对@JonSkeet 回答的评论中提到的那样,您还应该实施bool IEquatable&lt;T&gt;.Equals(T other) 以获得轻微的效率提升,但更重要的是提高清晰度。出于明显的原因,除了需要在 IEquatable&lt;T&gt; 旁边实现 GetHashCode 之外,IEquatable 的文档提到,为了保持一致性,您还应该覆盖 object.Equals 以保持一致性 我尝试实现这一点。 ovveride getHashcode 有效,但 override bool equals 得到错误:找不到可覆盖的方法。有什么想法吗? 终于找到了我想要的信息。谢谢。 来自我对上述答案的 cmets - 在您的“代替”的情况下,您可以拥有 public class a : IEqualityComparer&lt;a&gt; ,然后是 new HashSet&lt;a&gt;(a) 但请参阅上面的 Jon Skeets cmets。【参考方案3】:

HashSet 使用EqualsGetHashCode()

CompareTo 用于有序集。

如果您想要唯一的对象,但不关心它们的迭代顺序,HashSet&lt;T&gt; 通常是最佳选择。

【讨论】:

【参考方案4】:

构造函数 HashSet 接收实现 IEqualityComparer 以添加新对象的对象。 如果你想在 HashSet 中使用方法,你需要覆盖 Equals,GetHashCode

namespace HashSet

    public class Employe
    
        public Employe() 
        

        public string Name  get; set; 

        public override string ToString()  
            return Name;
        

        public override bool Equals(object obj) 
            return this.Name.Equals(((Employe)obj).Name);
        

        public override int GetHashCode() 
            return this.Name.GetHashCode();
        
    

    class EmployeComparer : IEqualityComparer<Employe>
    
        public bool Equals(Employe x, Employe y)
        
            return x.Name.Trim().ToLower().Equals(y.Name.Trim().ToLower());
        

        public int GetHashCode(Employe obj)
        
            return obj.Name.GetHashCode();
        
    
    class Program
    
        static void Main(string[] args)
        
            HashSet<Employe> hashSet = new HashSet<Employe>(new EmployeComparer());
            hashSet.Add(new Employe()  Name = "Nik" );
            hashSet.Add(new Employe()  Name = "Rob" );
            hashSet.Add(new Employe()  Name = "Joe" );
            Display(hashSet);
            hashSet.Add(new Employe()  Name = "Rob" );
            Display(hashSet);

            HashSet<Employe> hashSetB = new HashSet<Employe>(new EmployeComparer());
            hashSetB.Add(new Employe()  Name = "Max" );
            hashSetB.Add(new Employe()  Name = "Solomon" );
            hashSetB.Add(new Employe()  Name = "Werter" );
            hashSetB.Add(new Employe()  Name = "Rob" );
            Display(hashSetB);

            var union = hashSet.Union<Employe>(hashSetB).ToList();
            Display(union);
            var inter = hashSet.Intersect<Employe>(hashSetB).ToList();
            Display(inter);
            var except = hashSet.Except<Employe>(hashSetB).ToList();
            Display(except);

            Console.ReadKey();
        

        static void Display(HashSet<Employe> hashSet)
        
            if (hashSet.Count == 0)
            
                Console.Write("Collection is Empty");
                return;
            
            foreach (var item in hashSet)
            
                Console.Write("0, ", item);
            
            Console.Write("\n");
        

        static void Display(List<Employe> list)
        
            if (list.Count == 0)
            
                Console.WriteLine("Collection is Empty");
                return;
            
            foreach (var item in list)
            
                Console.Write("0, ", item);
            
            Console.Write("\n");
        
    

【讨论】:

如果名称为空怎么办? null 的哈希值是多少?【参考方案5】:

我来这里寻找答案,但发现所有答案信息太多或不够,所以这是我的答案......

由于您已经创建了一个自定义类,您需要实现GetHashCodeEquals。在这个例子中,我将使用一个类Student 而不是a,因为它更容易遵循并且不违反任何命名约定。 以下是实现的样子

public override bool Equals(object obj)

    return obj is Student student && Id == student.Id;


public override int GetHashCode()

    return HashCode.Combine(Id);

我偶然发现了this article from Microsoft,如果您使用的是 Visual Studio,它提供了一种非常简单的方法来实现这些。如果它对其他人有帮助,以下是使用 Visual Studio 在 HashSet 中使用自定义数据类型的完整步骤:

给定一个类Student,它有两个简单的属性和一个初始化器

public class Student

    public int Id  get; set; 
    public string Name  get; set; 

    public Student(int id)
    
        this.Id = id;
    
 

要实现 IComparable,请像这样添加 : IComparable&lt;Student&gt;

public class Student : IComparable<Student>

您会看到一条红色波浪线出现一条错误消息,指出您的类没有实现 IComparable。单击建议或按 Alt+Enter 并使用建议来实现它。

您将看到生成的方法。然后,您可以编写自己的实现,如下所示:

public int CompareTo(Student student)

    return this.Id.CompareTo(student.Id);

在上面的实现中,只比较了 Id 属性,忽略了 name。接下来右键单击您的代码并选择Quick actions and refactorings,然后Generate Equals and GetHashCode

将弹出一个窗口,您可以在其中选择用于散列的属性,如果您愿意,甚至可以实现 IEquitable:

这是生成的代码:

public class Student : IComparable<Student>, IEquatable<Student> 
    ...
    public override bool Equals(object obj)
    
        return Equals(obj as Student);
    

    public bool Equals(Student other)
    
        return other != null && Id == other.Id;
    

    public override int GetHashCode()
    
        return HashCode.Combine(Id);
    

现在,如果您尝试添加如下所示的重复项目,它将被跳过:

static void Main(string[] args)

    Student s1 = new Student(1);
    Student s2 = new Student(2);
    HashSet<Student> hs = new HashSet<Student>();

    hs.Add(s1);
    hs.Add(s2);
    hs.Add(new Student(1)); //will be skipped
    hs.Add(new Student(3));

您现在可以像这样使用.Contains

for (int i = 0; i <= 4; i++)

    if (hs.Contains(new Student(i)))
    
        Console.WriteLine($@"Set contains student with Id i");
    
    else
    
        Console.WriteLine($@"Set does NOT contain a student with Id i");
    

输出:

【讨论】:

以上是关于HashSet 如何比较元素是不是相等?的主要内容,如果未能解决你的问题,请参考以下文章

为什么重写equals以后还要重写hashcode

Java HashSet

HashSet

您如何确定两个 HashSet 是不是相等(按值,而不是按引用)?

集合04_Set

HashSet实现不重复储值原理-附源码解析