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

我有一门课是 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

  1. 为什么 HashSet不尊重 aCompareTo方法。
  2. HashSet是拥有唯一对象列表的最佳方式吗?
108226 次浏览

HashSet使用 EqualsGetHashCode()

CompareTo表示有序集。

如果您想要唯一的对象,但是您不关心它们的迭代顺序,那么 HashSet<T>通常是最好的选择。

它使用 IEqualityComparer<T>(EqualityComparer<T>.Default,除非在构造上指定不同的 EqualityComparer<T>.Default)。

当您向集合中添加一个元素时,它将使用 IEqualityComparer<T>.GetHashCode查找哈希代码,并存储哈希代码和元素(当然是在检查元素是否已经在集合中之后)。

为了查找一个元素,它首先使用 IEqualityComparer<T>.GetHashCode来查找哈希代码,然后对于具有相同哈希代码的所有元素,它将使用 IEqualityComparer<T>.Equals来比较实际的相等性。

这意味着你有两个选择:

  • 将自定义 IEqualityComparer<T>传递到构造函数中。如果不能修改 T本身,或者想要一个非默认的相等关系(例如“所有用户 ID 为负的用户都被认为是相等的”) ,那么这是最好的选择。这几乎从未在类型本身上实现(即 Foo不实现 IEqualityComparer<Foo>) ,而是在一个单独的类型中实现,这个类型只用于比较。
  • 通过重写 GetHashCodeEquals(object),在类型本身中实现相等性。理想情况下,在类型中也实现 IEquatable<T>,特别是当它是值类型时。这些方法将由默认的相等比较器调用。

请注意,这些都与 命令比较无关——这是有意义的,因为在某些情况下,您可以轻松地指定相等性,但不能指定总次序。这和 Dictionary<TKey, TValue>基本上是一样的。

如果您想要一个使用 点餐而不仅仅是相等比较的集合,那么您应该使用。NET 4-它允许您指定一个 IComparer<T>而不是 IEqualityComparer<T>。这将使用 IComparer<T>.Compare-如果您使用 Comparer<T>.Default,它将委托给 IComparable<T>.CompareToIComparable.CompareTo

以下是对部分未提及的答案的说明: HashSet<T>的对象类型不必实现 IEqualityComparer<T>,只需覆盖 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<a>在使用该设置时,最终将根据需要调用 a.GetHashCode()a.Equals(obj)

构造函数 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");
}
}
}

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

因为您已经创建了一个自定义类,所以需要实现 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);
}

我偶然发现了 这篇来自微软的文章,它提供了一种难以置信的简单方法来实现这些,如果您正在使用 VisualStudio 的话。如果对其他人有帮助,下面是使用 Visual Studio 在 HashSet 中使用自定义数据类型的完整步骤:

给定一个具有2个简单属性和一个初始值设定项的类 Student

public class Student
{
public int Id { get; set; }
public string Name { get; set; }


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

要实现 Icomable,请像下面这样添加 : IComparable<Student>:

public class Student : IComparable<Student>

您将看到一个红色的波浪形出现,其中包含一条错误消息,表明您的类没有实现 ICompable。单击建议或按 Alt + Enter 并使用建议来实现它。

use the suggestion to implement IComparable

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

public int CompareTo(Student student)
{
return this.Id.CompareTo(student.Id);
}

在上面的实现中,只比较 Id 属性,忽略 name。接下来右键单击代码并选择 快速操作和重构,然后选择 生成 Equals 和 GetHashCode

Generate Equals and GetHashCode

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

pop up where you can select which properties to use for hashing

下面是生成的代码:

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}");
}
}

产出:

Console output