为什么要实现IEquatable<T>接口

5

我一直在阅读有关接口的文章,并且了解了它们一定程度上的用法。然而,如果我想编写自己的自定义Equals方法,似乎可以不必实现IEquatable接口。以下是一个示例。

using System;
using System.Collections;
using System.ComponentModel;

namespace ProviderJSONConverter.Data.Components
{
    public class Address : IEquatable<Address>
    {
        public string address { get; set; }

        [DefaultValue("")]
        public string address_2 { get; set; }
        public string city { get; set; }
        public string state { get; set; }
        public string zip { get; set; }

        public bool Equals(Address other)
        {
            if (Object.ReferenceEquals(other, null)) return false;
            if (Object.ReferenceEquals(this, other)) return true;
            return (this.address.Equals(other.address)
                && this.address_2.Equals(other.address_2)
                && this.city.Equals(other.city)
                && this.state.Equals(other.state)
                && this.zip.Equals(other.zip));
        }
    }
 }

现在,如果我不实现接口并将: IEquatable<Address>从代码中删除,似乎应用程序的操作完全相同。因此,我不清楚为什么要实现该接口?我可以自己编写自定义Equals方法而不需要它,断点仍将命中该方法并返回相同的结果。
有人能帮我更好地解释一下吗?我困惑于为什么在调用Equals方法之前要包含"IEquatable<Address>"。

2
顺便提一下:现在是开始遵循.NET命名约定的好时机;可变类型的相等性可能会导致问题;你也应该重写Equals(object)GetHashCode - Jon Skeet
@haim770:OP没有重写Object.Equals,所以我认为这不是同一个问题。 - Jon Skeet
1
@haim770:不,我不这么认为。换句话说,我在那里的任何答案中都没有看到我提供的信息。 - Jon Skeet
@JonSkeet 我理解"可以自己编写自定义的Equals方法"是OP对基本的Object.Equals/Object.GetHashCode的使用和重写有所了解... 可能我理解错了,所以请随意重新打开(并可能编辑帖子)。 - Alexei Levenkov
1
我重新打开了它,因为原帖作者没有覆盖Equals方法,所以这不是一个重复的问题。 - ken2k
显示剩余3条评论
4个回答

15

现在,如果我不实现该接口并将IEquatable从代码中删除,似乎应用程序的操作完全相同。

好的,这取决于"应用程序"做什么。例如:

List<Address> addresses = new List<Address>
{
    new Address { ... }
};
int index = addresses.IndexOf(new Address { ... });

如果您既没有重写Equals(object),也没有实现IEquatable<T>,那么这不起作用(即index将为-1)。List<T>.IndexOf不会调用您的Equals重载。

知道您的具体类的代码将使用Equals重载 - 但任何代码(例如泛型集合、所有LINQ to Objects等)仅使用任意对象的代码将不会使用它。


谢谢您的解释。我刚刚测试了代码,我明白您的意思了。如果没有接口,等于方法就不会被使用。 - devzim
Justin Weinzimmer:「没有接口,等于方法就不被利用」这有点误导:如果你重写Equals(object),你可以在没有接口的情况下使用它。实际上,如果你实现了IEquatable <T>,你必须同时重写Equals(object);否则,在不同的情境中会以不同的方式处理你的类型。请参阅我在此处的评论:https://dev59.com/c47ea4cB1Zd3GeqPFcAS#32380596 - György Kőszeg
我在上面编写的equals方法的方式...它不会被使用,我知道可以重写对象equals方法,但是在这里我没有这样做。 - devzim

10
.NET框架有许多关于相等性检查的可能性,容易令人困惑:
  1. 虚拟的Object.Equals(object)
  2. 可重载的相等运算符(==, !=, <=, >=)
  3. IEquatable<T>.Equals(T)
  4. IComparable.CompareTo(object)
  5. IComparable<T>.CompareTo(T)
  6. IEqualityComparer.Equals(object, object)
  7. IEqualityComparer<T>.Equals(T, T)
  8. IComparer.Compare(object, object)
  9. IComparer<T>.Compare(T, T)

此外,我没有提到ReferenceEquals,静态的Object.Equals(object, object)以及特殊情况(例如字符串和浮点数比较),只是一些我们可以实现的情况。

另外,第一和第二种情况的默认行为对于结构体和类是不同的。因此,用户可能会对何时以及如何实现感到困惑。

作为一个经验法则,您可以遵循以下模式:

  • 默认情况下,Equals(object)方法和相等运算符(==, !=)都检查引用相等性
  • 如果引用相等性不适合您,请重写Equals方法(并且也要重写GetHashCode;否则,您的类将无法用于哈希集合中)。
  • 您可以保留==!=运算符的原始引用相等性功能,这对于类来说很常见。但是如果您重载它们,它必须与Equals一致。
  • 如果您的实例可以在意义上进行比较,则实现IComparable接口。当Equals报告相等时,CompareTo必须返回0(再次强调一致性)。

基本上就是这样。为类实现泛型IEquatable<T>Comparable<T>接口并不是必须的:由于没有装箱,泛型集合中的性能提升将是微小的。但是请记住,如果您实现它们,请保持一致性

结构体

  • 结构体默认情况下,Equals(object) 执行的是一个值比较(检查字段值)操作。尽管在值类型的情况下通常这是预期行为,但基本实现使用反射来执行此操作,性能非常差。因此,即使实现了与原始函数相同的功能,请在公共结构中始终覆盖 Equals(object)
  • 当对结构体使用 Equals(object) 方法时,会发生装箱操作,这会带来一定的性能成本(不像 ValueType.Equals 中的反射那样糟糕,但它确实很重要)。这就是为什么 IEquatable<T> 接口存在的原因。如果要在泛型集合中使用结构体,则应在其上实现该接口。我已经提到过保持一致性了吗?
  • 默认情况下,不能使用 ==!= 运算符来处理结构体,因此如果想使用它们,则必须重载这些运算符。只需调用强类型的 IEquatable<T>.Equals(T) 实现即可。
  • 与类类似,如果小于或大于对您的类型有意义,则实现 IComparable 接口。在结构体的情况下,您还应该实现 IComparable<T> 以使事情变得高效(例如,在 Array.SortList<T>.BinarySearch 中使用类型作为键,在 SortedList<TKey, TValue> 中使用等)。如果重载了 ==!= 运算符,则也应该重载 <><=>= 这些运算符。

一个小附加说明: 如果必须使用具有不合适比较逻辑的类型,则可以在列表中使用第6到9个接口。这是您可以忽略一致性(至少考虑到类型自身的 Equals)并实现可用于基于哈希和排序的集合的自定义比较的地方。


2
如果你覆盖了Equals(object obj)方法,那么这只是一个性能问题,如此处所述:IEquatable和仅覆盖Object.Equals()之间有什么区别? 但只要你没有覆盖Equals(object obj)但提供了自己的强类型Equals(Adddress obj)方法,并且没有实现IEquatable<T>,你就没有向依赖于该接口实现进行比较操作的所有类表明你有自己的Equals方法应该被使用。
因此,正如John Skeet所指出的那样,List<Address>.IndexOf使用的EqualityComparer<Address>.Default属性将无法知道它应该使用你的Equals方法。

2

IEquatable接口只是添加了Equals方法,其中泛型参数可以是任何类型。然后函数重载会处理其余部分。

如果我们将IEquatable添加到Employee结构中,则该对象可以与Employee对象进行比较,无需任何类型转换。尽管使用默认的Equals方法也可以实现相同的功能,该方法接受Object作为参数,但从Object转换为结构需要装箱。因此,具有IEquatable<Employee>将提高性能。

例如,假设我们想将Employee结构与另一个employee进行比较。

if(e1.Equals(e2))
{
   //do some
}

对于上述示例,它将使用 Employee 作为参数的 Equals 方法。因此不需要装箱或拆箱。

 struct Employee : IEquatable<Employee>
{
    public int Id { get; set; }

    public bool Equals(Employee other)
    {
        //no boxing not unboxing, direct compare
        return this.Id == other.Id;
    }

    public override bool Equals(object obj)
    {
        if(obj is Employee)
        {   //un boxing
            return ((Employee)obj).Id==this.Id;
        }
        return base.Equals(obj);
    }
}

更多示例:

Int结构实现了IEquatable <int>

Bool结构实现了IEquatable <bool>

Float结构实现了IEquatable <float>

因此,如果您调用someInt.Equals(1),它不会触发Equals(object)方法,而是触发Equals(int)方法。


1
你有Employee:IEquatable<int>的实际使用示例吗?我没有看到任何.Net类使用这样的结构(不像Employee:IEquatable<Employee>)。 - Alexei Levenkov
@AlexeiLevenkov,我已经更新了示例和答案。感谢您的指出。 - CreativeManix

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