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

问题描述 投票:0回答:5

我有一堂课是

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.count
2
,但是:

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.count
3

  1. 为什么
    HashSet
    不尊重
    a
    CompareTo
    方法。
  2. HashSet
    是拥有唯一对象列表的最佳方式吗?
c# hashset
5个回答
188
投票

它使用

IEqualityComparer<T>
EqualityComparer<T>.Default
,除非您在构造时指定不同的)。

当您向集合中添加元素时,它将使用

IEqualityComparer<T>.GetHashCode
查找哈希码,并存储哈希码和元素(当然,在检查该元素是否已在集合中之后)。

要查找元素,它将首先使用

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

这意味着您有两个选择:

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

请注意,这些都不是 ordered 比较 - 这是有道理的,因为在某些情况下,您可以轻松指定相等性,但不能指定总排序。这基本上与

Dictionary<TKey, TValue>
相同。

如果您想要一个使用 ordering 而不仅仅是相等比较的集合,您应该使用 .NET 4 中的

SortedSet<T>
- 它允许您指定
IComparer<T>
而不是
IEqualityComparer<T>
。这将使用
IComparer<T>.Compare
- 如果您使用
IComparable<T>.CompareTo
,它将委托给
IComparable.CompareTo
Comparer<T>.Default


98
投票

这里对未提及的部分答案进行澄清:

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)


16
投票

HashSet
使用
Equals()
GetHashCode()

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

7
投票

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

既然您创建了一个自定义类,您需要实现

GetHashCode
Equals
。在此示例中,我将使用类
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 并使用建议来实现它。

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

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

在上面的实现中,仅比较 Id 属性,忽略 name。接下来右键单击您的代码并选择 快速操作和重构,然后选择 生成等于和 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}");
    }
}

输出:

© www.soinside.com 2019 - 2024. All rights reserved.