HashSet如何比较元素的相等性?

161

我有一个实现了 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是拥有唯一对象列表的最佳方式吗?

在构造函数中添加 IEqualityComparer<T> 的实现或在类 a 中实现它。https://msdn.microsoft.com/zh-cn/library/bb301504(v=vs.110).aspx - Jaider
5个回答

181
它使用一个IEqualityComparer<T>(如果没有在构造函数上指定其他内容,则使用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中的SortedSet<T> - 它允许您指定IComparer<T>而不是IEqualityComparer<T>。这将使用IComparer<T>.Compare - 如果使用Comparer<T>.Default,则将委托给IComparable<T>.CompareToIComparable.CompareTo

7
还请注意@tyriker的答案(在我看来应该是这里的评论),他指出利用IEqualityComparer<T>.GetHashCode/Equals()最简单的方法是在T本身上实现EqualsGetHashCode(在此过程中,您还应该实现强类型对应项:bool IEquatable<T>.Equals(T other))。 - Ruben Bartelink
5
尽管这个答案非常准确,但对于新用户来说可能有些令人困惑,因为它没有清楚地说明对于最简单的情况,覆盖EqualsGetHashCode就已足够 - 正如@tyriker的答案所提到的。 - BartoszKP
@nawfal 并不是所有的事情都有逻辑顺序。如果你正在比较两个包含布尔属性的东西,那么写类似 a.boolProp == b.boolProp ? 1 : 0 或者应该是 a.boolProp == b.boolProp ? 0 : -1 或者 a.boolProp == b.boolProp ? 1 : -1 这样的代码就显得非常糟糕了。呸! - Simon_Weaver
1
@Simon_Weaver 是的。我确实希望在我提出的假设功能中以某种方式避免它。 - nawfal
@JonSkeet 恭喜您获得100万声望。为什么您的答案不起作用,而 @tyriker 的答案可以 - 即不实现 IEqualityComparer<T>?我发现使用 IEqualityComparer<T> 时,Equals(T a, T b) 从未被调用。但是重写的版本却被调用了。 - HankCa
显示剩余11条评论

97

这里是对答案中未提到的部分的澄清:您的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)


2
好的观点。顺便提一下,在@JonSkeet的答案评论中提到,为了稍微提高效率,但更重要的是为了清晰度的好处,您还应该实现bool IEquatable<T>.Equals(T other)。出于明显的原因,除了需要在IEquatable<T>旁边实现GetHashCode之外,IEquatable<T>的文档还提到,为了一致性,您还应该覆盖object.Equals - Ruben Bartelink
根据我对上面答案的评论 - 在您的“代替”情况下,您可以使用public class a:IEqualityComparer<a> {,然后使用new HashSet<a>(a) - HankCa
但请参考Jon Skeet上面的评论。 - HankCa
IEqualityComparer 是由字典使用的,还是覆盖也适用于字典? - WDUK
你明白IEqualityComparer不是由对象实现而是一个独立的类,对吧?如果用户可以访问对象代码,则要实现的接口是IEquatable<>。 - Lucas Montenegro Carvalhaes
显示剩余4条评论

15

HashSet使用EqualsGetHashCode()

CompareTo用于有序集合。

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


7

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

1
如果名称为null怎么办?null的哈希值是多少? - joe

6

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

既然你已经创建了一个自定义类,你需要实现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);
}

我发现这篇来自Microsoft的文章,如果你正在使用Visual Studio,那么实现这些功能的方法非常简单。如果对其他人有帮助的话,下面是在Visual Studio中使用自定义数据类型在HashSet中的完整步骤:
假设有一个类Student,其中包含2个简单属性和一个初始化器。
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }

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

要实现IComparable,只需添加: IComparable<Student>即可:
public class Student : IComparable<Student>

您将看到一个红色波浪线,上面显示一个错误消息,说明您的类没有实现IComparable接口。点击建议或按Alt+Enter键,并使用建议来实现它。

use the suggestion to implement IComparable

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

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

在上述实现中,仅比较了Id属性,忽略了名称。接下来,在您的代码中右键单击并选择“快速操作和重构”,然后选择“生成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


1
太好了,谢谢你。我之前有些困惑其他答案,正如你所指出的,它已经内置在Visual Studio中了。 - mejobloggs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接