C# IEquatable和IEqualityComparer 最佳实践

前言

IEquatable<T>IEqualityComparer<T>C# 中用于比较对象的接口,它们有以下区别:

  • IEquatable
    IEquatable<T> 是一个泛型接口,定义了一个用于比较对象相等性的方法 Equals(T other)
    当你想要在类中自定义相等性比较的逻辑时,可以实现 IEquatable<T> 接口。
    实现了 IEquatable<T> 接口的类可以通过调用 Equals 方法来进行对象之间的相等性比较。
  • IEqualityComparer
    IEqualityComparer<T> 也是一个泛型接口,定义了两个方法:Equals(T x, T y)GetHashCode(T obj)
    当你需要自定义对象之间的相等性比较逻辑和哈希码计算逻辑时,可以实现 IEqualityComparer<T> 接口。

通常场景他们使用效果是一样的,例如进行Enumerable.Distinct

IEquatable<T>

IEquatable<T>C# 中用于比较对象相等性的泛型接口。它定义了一个名为 Equals 的方法,用于比较对象与另一个对象的相等性。

通过实现 IEquatable<T> 接口,你可以在类中自定义对象相等性的比较逻辑。这使得你的类更具灵活性,可以根据你的需求来确定两个对象是否相等。

根据MS推荐实现了Equals最好也实现他的操作符==!=

using System;
using System.Collections.Generic;
using System.Xml.Linq;

static class Example
{
    static void Main()
    {
        List<Person> personList = new List<Person>()
        {
            new Person() { Name = "A", Age = 1,  },
            new Person() { Name = "B", Age = 2,  },
            new Person() { Name = "B", Age = 2,  },
            new Person() { Name = "C", Age = 3,  },
            new Person() { Name = "C", Age = 3,  },
        };

        IEnumerable<Person> nameDisList = personList.Distinct(); // 实现了 IEquatable 按照 Name + Age 进行去重

        // {Name=A, Age=1},{Name=B, Age=2},{Name=C, Age=3}
        Console.WriteLine(string.Join(",", nameDisList));

        Console.ReadKey();
    }
}

public class Person : IEquatable<Person>
{
    public int Age
    {
        get;
        set;
    }

    public string Name
    {
        get;
        set;
    }

    public override bool Equals(object obj)
    {
        return this.Equals(obj as Person); // 调用内部的对比即可
    }

    public bool Equals(Person other)
    {
        if (other == null)
            return false;

        // Optimization for a common success case.
        if (ReferenceEquals(this, other))
            return true;

        if (this.Age == other.Age && this.Name == other.Name)
            return true;
        else
            return false;
    }

    public override int GetHashCode()
    {
        // 选择两个不同的质数,例如 17 和 23
        // 关于质数,可以参考 https://en.wikipedia.org/wiki/Prime_number
        // 重写 GetHashCode() 方法最佳实践请见 https://stackoverflow.com/questions/263400/what-is-the-best-algorithm-for-overriding-gethashcode
        int hashCode = 17;
        hashCode = hashCode * 23 + EqualityComparer<string>.Default.GetHashCode(this.Name);
        hashCode = hashCode * 23 + EqualityComparer<int>.Default.GetHashCode(this.Age);

        return hashCode;
    }

    public static bool operator ==(Person p1, Person p2)
    {
        if (p1 is null && p2 is null) // 一定不要操作符重写中使用 == 操作符来判断两个对象是否相等,否则会堆栈溢出
            return true;

        if (((object)p1) == null || ((object)p2) == null)
            return Equals(p1, p2);

        return p1.Equals(p2);
    }

    public static bool operator !=(Person p1, Person p2)
    {
        return !(p1 == p2); // 使用不等于即可
    }
}

IEqualityComparer<T>

IEqualityComparer<T>C# 中用于比较对象相等性的泛型接口。它定义了两个方法:Equals(object x, object y)GetHashCode(object obj)

Equals(object x, object y) 方法用于比较两个对象 xy 是否相等。它返回一个布尔值,指示两个对象是否相等。
GetHashCode(object obj) 方法用于计算对象 obj 的哈希码。哈希码是一个整数,用于在哈希表等数据结构中进行快速查找和比较。
通常情况下,IEqualityComparer 接口用于在没有泛型的情况下,提供自定义的对象相等性比较和哈希码计算逻辑。 你可以实现 IEqualityComparer 接口来定义你自己的比较器,以便在需要自定义对象相等性比较的情况下使用。

using System;
using System.Collections.Generic;
using System.Xml.Linq;

static class Example
{
    static void Main()
    {
        List<Person> personList = new List<Person>()
        {
            new Person() { Name = "A", Age = 1,  },
            new Person() { Name = "B", Age = 2,  },
            new Person() { Name = "B", Age = 3,  },
            new Person() { Name = "C", Age = 4,  },
            new Person() { Name = "B", Age = 4,  },
        };

        IEnumerable<Person> nameDisList = personList.Distinct(new PersonNameEqualityComparer());
        IEnumerable<Person> ageDisList = personList.Distinct(new PersonAgeEqualityComparer());

        // {Name=A, Age=1},{Name=B, Age=2},{Name=C, Age=4}
        Console.WriteLine(string.Join(",", nameDisList));
        // {Name = A, Age = 1},{ Name = B, Age = 2},{ Name = B, Age = 3},{ Name = C, Age = 4}
        Console.WriteLine(string.Join(",", ageDisList));

        Console.ReadKey();
    }
}

class PersonNameEqualityComparer : IEqualityComparer<Person>
{
    public bool Equals(Person p1, Person p2)
    {
        if (ReferenceEquals(p1, p2))
            return true;
        if (p2 is null || p1 is null)
            return false;

        return p1.Name == p2.Name;
    }

    public int GetHashCode(Person p) => p.Name.GetHashCode();
}

class PersonAgeEqualityComparer : IEqualityComparer<Person>
{
    public bool Equals(Person p1, Person p2)
    {
        if (ReferenceEquals(p1, p2))
            return true;
        if (p2 is null || p1 is null)
            return false;
            
        return p1.Age == p2.Age;
    }

    public int GetHashCode(Person p) => p.Age.GetHashCode();
}

MS不建议直接派生IEqualityComparer<T>,而是使用EqualityComparer<T>,本例代码可以直接改成EqualityComparer

建议从 EqualityComparer 类派生,而不是实现 IEqualityComparer 接口,因为 EqualityComparer 类使用 IEquatable.Equals 方法而不是 Object.Equals 方法测试相等性。 这与 Contains类和其他泛型集合的 Dictionary<TKey,TValue> 、IndexOf、 LastIndexOf和 Remove 方法一致。

如果是单纯的去重,高版本的C#可以使用Enumerable.DistinctBy

后言

通过上面介绍我们了解了它们两种的使用方式,那么什么时候用IEqualityComparer<T> 什么时候用IEquatable<T>呢?

  1. 当目标类你没有源代码,无法直接修改的时候请使用IEqualityComparer<T>
  2. 当目标类你只需要一种对比方式可以考虑使用IEquatable<T>
  3. 当目标类有多重对比方式,例如根据AB或者BC等多重属性进行对比,你可以定义多个IEqualityComparer<T>
  4. 当然两种方式可以共存,根据需要使用

关于 HashCode

对于GetHashCode方法来说我们即使返回的是不同的值,但是也有可能会出现哈希冲突,所以我们需要尽量减少哈希冲突的概率,这样可以提高哈希表的性能。
即使你返回相同的值,最终效果也是一样的,因为它都会走一遍对比逻辑,性能会低很多。

对于高版本可以使用ValueTuple,值元祖不会在堆上创建垃圾对象。
低版本可以使用匿名类型,使用匿名类型的原始技术会在堆上创建一个对象,也就是垃圾,因为匿名类型是作为类实现的。

// ValueTuple
(PropA, PropB, PropC, PropD).GetHashCode();

// Anonymous Type
new { PropA, PropB, PropC, PropD }.GetHashCode();

HashCode最佳实践可以参考stackoverflow这篇文章。

参考

posted @ 2024-01-04 11:06  陈大欠  阅读(832)  评论(0编辑  收藏  举报