何时使用IEquatable<T>以及原因

58

IEquatable<T> 究竟为您带来了什么?我唯一能想到的有用之处是在创建泛型类型并强制用户实现和编写一个良好的等于方法时。

我错过了什么吗?


1
它可以帮你省去装箱/拆箱处理。这里有一个好的简单解释:http://www.codeproject.com/Articles/20592/Implementing-IEquatable-Properly - Jviaches
4个回答

53

来自MSDN的介绍:

IEquatable(T)接口用于泛型集合对象,如Dictionary(TKey, TValue)List(T)LinkedList(T)等,用于在ContainsIndexOfLastIndexOfRemove等方法中进行相等性测试。

IEquatable<T>实现类将需要较少的强制转换,因此比否则使用的标准object.Equals方法略快。例如,看一下这两种方法的不同实现:

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

  return (this.Id == other.Id);
}

public override bool Equals(Object obj)
{
  if (obj == null) 
     return false;

  T tObj = obj as T;  // The extra cast
  if (tObj == null)
     return false;
  else   
     return this.Id == tObj.Id;
}

4
如果在使用泛型集合时不实现IEquatable,是否有人知道会发生什么? - Jack Kada
2
@ChloeRadshaw 如果是引用类型,它将使用引用相等。 - Cornelius
1
它将在不实现IEquatable<T>的情况下与上述类型一起使用;它将仅使用默认的Equals()。话虽如此,如果可用,它似乎会使用IEquatable<T>,并避免Drew Freyling提到的装箱/拆箱。 - Doug
必须重写GetHashCode方法,否则与列表和其他集合的工作将会出现问题。 - Siarhei Kuchuk
1
EqualityComparer<T>.Default使用IEquatable<T>.Equals。 - Matus
显示剩余2条评论

50

我惊讶的是最重要的原因没有被提到。

IEquatable<>主要是为了结构体而引入的,有两个原因:

  1. 对于值类型(即结构体),非泛型的Equals(object)需要装箱。 IEquatable<>允许结构体实现一个强类型的Equals方法,从而无需装箱。

  2. 对于结构体,默认实现的Object.Equals(Object)(这是在System.ValueType中重写的版本)通过使用反射来比较类型中每个字段的值来执行值相等性检查。当实现者在结构中重写虚拟Equals方法时,其目的是提供一种更有效的执行值相等性检查并可选择地将比较基于结构体的某些字段或属性子集的方法。

这两个原因都可以提高性能。

参考类型(即类)受益不如此明显。 IEquatable<>的实现确实可以避免您从System.Object进行强制转换,但这只是微不足道的收益。尽管如此,我仍然喜欢为我的类实现IEquatable<>,因为这从逻辑上明确了意图。


1
为什么需要IEquatable<>来避免装箱?你可以创建自己的方法bool Equals(StructNameHere yourStruct){...},这将避免装箱。 - David Klempfner
3
@Backwards_Dave 当然可以,但是:1. IEquatable 接口使意图更加清晰(比如考虑引用的库类源代码不可用的情况);2. 您可以将您的结构体传递给接受 IEquatable 接口或者在泛型约束上使用 IEquatable<T> 的方法中;3. 最重要的是,大多数集合操作的实现,例如 Contains 等,都不会调用您自定义的 Equals 方法(它们都检查类型是否实现了 IEquatbale 接口,否则就调用基类的 ValueType.Equals 方法)。 - nawfal
关于最后一点,框架可以利用鸭子类型仅检查 Equals(T) 的实现,但这会增加更多的性能成本。在编程中通常按照约定来处理事物。在 .NET 中,如果要修改结构体的相等性,则应实现 IEquatable(不要忘记实现 GetHashCode==!=)以与之配合使用。 - nawfal
1
在这里查看“EquatableStruct”和“PlainStruct”的行为差异:https://dotnetfiddle.net/FTzETU - nawfal

3

除了其他答案之外,这里有一个非常好的原因来实现IEquatable<T>(并显然覆盖Equals(object))适用于值类型。只需查看默认的ValueType.Equals(object)代码,否则就会调用它。这是一个绝对的性能杀手,引入装箱,类型评估,并最终回退到反射如果任何字段是引用类型。

public override bool Equals(object obj)
{
    if (obj == null)
    {
        return false;
    }
    RuntimeType type = (RuntimeType) base.GetType();
    RuntimeType type2 = (RuntimeType) obj.GetType();
    if (type2 != type)
    {
        return false;
    }
    object a = this;
    if (CanCompareBits(this))
    {
        return FastEqualsCheck(a, obj);
    }
    FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
    for (int i = 0; i < fields.Length; i++)
    {
        object obj3 = ((RtFieldInfo) fields[i]).UnsafeGetValue(a);
        object obj4 = ((RtFieldInfo) fields[i]).UnsafeGetValue(obj);
        if (obj3 == null)
        {
            if (obj4 != null)
            {
                return false;
            }
        }
        else if (!obj3.Equals(obj4))
        {
            return false;
        }
    }
    return true;
}

在某些情况下(例如在字典中使用值类型作为键),这可能会一次性损害性能。

然而,如果没有引用类型字段,则这比按字段比较快得多。 - Evgeny Gorbovoy

0
根据文档,IEquality<T> 用于提高性能(它可以防止装箱)。在泛型集合中尤其有用。
如果你想在类层次结构中实现 IEquatable<T> ,你可以使用以下模式。它可以防止派生类(包括兄弟类)相等。如果不需要派生类的相等性,则可以跳过 IEquatable<Derived> 但必须重写 CanEqual 以防止它与基类相等 (除非当然它们应该被视为相等)。
虽然我认为避免装箱带来的收益会比拥有 CanEqual 带来的成本少。在这种情况下,您应该封闭您的类型,那么您就不再需要 CanEqual。封闭还具有一些性能优势。
public class Base : IEquatable<Base>
{
    protected virtual bool CanEqual(Base other) => other is Base;

    public override bool Equals(object obj) => obj is Base other && Equals(other);

    public bool Equals(Base other) => this.CanEqual(other) && other.CanEqual(this) /* && base logic */; 
}

public class Derived : Base, IEquatable<Derived>
{
    protected override bool CanEqual(Base other) => other is Derived;

    public override bool Equals(object obj) => obj is Derived other && Equals(other);

    public bool Equals(Derived other) => this.CanEqual(other) && other.CanEqual(this) /* && derived logic */;
}

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