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

nim*_*ima 114 c# 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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);
Run Code Online (Sandbox Code Playgroud)

一切都很好,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));
Run Code Online (Sandbox Code Playgroud)

现在ha.count3.

  1. 为什么不HashSet尊重aCompareTo方法.
  2. HashSet拥有唯一对象列表的最佳方法是什么?

Jon*_*eet 123

它使用一个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>在类型中实现,特别是如果它是值类型.这些方法将由默认的相等比较器调用.

请注意,对于有序比较而言,这一切都没有- 这是有道理的,因为在某些情况下您可以轻松指定相等但不是总排序.这Dictionary<TKey, TValue>基本上都是一样的.

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

  • +1还要注意@ tyriker的答案(IMO应该是这里的评论),它指出利用所谓的"IEqualityComparer <T> .GetHashCode/Equals()"的最简单方法是实现`Equals`和`GetHashCode`. T`本身(当你这样做的时候,你也会实现强类型的对应物: - `bool IEquatable <T> .Equals(T other)`) (6认同)
  • 虽然非常准确,但这个答案可能有点令人困惑,特别是对于新用户,因为它没有明确说明,对于最简单的情况,重写"Equals"和"GetHashCode"就足够了 - 正如@ tyriker的回答中提到的那样. (4认同)

tyr*_*ker 70

这里澄清了一些未说明的答案:你的对象类型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 */ }
}
Run Code Online (Sandbox Code Playgroud)

你做这个:

public class a
{
  public override int GetHashCode() { /* Implementation */ }
  public override bool Equals(object obj) { /* Implementation */ }
}
Run Code Online (Sandbox Code Playgroud)

这很微妙,但这让我感到震惊,因为我想要让HashSet按照预期的方式运行.就像其他人所说的那样,在使用该套装时,HashSet<a>最终会打电话a.GetHashCode()a.Equals(obj)根据需要.

  • 好点子.在我对@ JonSkeet的回答中提到的BTW中,您还应该实现`bool IEquatable <T> .Equals(T other)`以获得轻微的效率增益,但更重要的是清晰度的好处.出于显而易见的原因,除了需要在`IEquatable <T>`之外实现`GetHashCode`之外,IEquatable <T>的文档提到为了保持一致性,你还应该覆盖`object.Equals`以保持一致性 (2认同)
  • 但为什么?这非常不直观. (2认同)

Cod*_*aos 9

HashSet用途EqualsGetHashCode().

CompareTo 是订购套装.

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


tec*_*n23 7

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

由于您已经创建了一个自定义类,因此您需要实现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);
}
Run Code Online (Sandbox Code Playgroud)

我偶然发现了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;
    }
 }
Run Code Online (Sandbox Code Playgroud)

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

public class Student : IComparable<Student>
Run Code Online (Sandbox Code Playgroud)

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

使用建议来实现 IComparable

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

public int CompareTo(Student student)
{
    return this.Id.CompareTo(student.Id);
}
Run Code Online (Sandbox Code Playgroud)

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

生成等于和 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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));
}
Run Code Online (Sandbox Code Playgroud)

.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}");
    }
}
Run Code Online (Sandbox Code Playgroud)

输出:

控制台输出

  • 太棒了,谢谢。我对其他答案有点挣扎,然后正如你所指出的,它无论如何都内置在 Visual Studio 中 (2认同)

小智 6

构造函数 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");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)