非密封类是否应该实现IEquatable<T>和IComparable<T>接口?

38
有没有人对于IEquatable<T>或者IComparable<T>是否应该要求Tsealed(如果它是一个class)有任何意见?
我之所以会想到这个问题,是因为我正在编写一组基类,旨在帮助实现不可变类的实现。其中一部分功能是自动实现相等比较(使用类的字段以及可以应用于字段的属性来控制相等比较)。完成后应该会很好用 - 我使用表达式树为每个T动态创建编译的比较函数,因此比较函数应该非常接近普通相等比较函数的性能。(我使用了一个不可变字典,以System.Type为键,并使用双重检查锁定的方式存储生成的比较函数,以使其性能合理)
但是出现了一个问题,那就是要使用哪些函数来检查成员字段的相等性。我的初衷是检查每个成员字段的类型(我将其称为X)是否实现了IEquatable<X>。然而,在考虑了一段时间之后,我认为除非Xsealed,否则不能安全地使用这种方法。原因是如果X没有sealed,我无法确定X是否适当地将相等性检查委托给了X上的虚拟方法,从而允许子类型覆盖相等比较。
这带来了一个更一般的问题 - 如果一个类型没有被sealed,那么它真的应该实现这些接口吗?我认为不应该,因为我认为接口的约定是在两个X类型之间进行比较,而不是在可能是X或子类型的两个类型之间进行比较。
你们觉得呢?IEquatable<T>IComparable<T>是否应该避免用于未封闭的类?(还让我想到了是否有针对此的fxcop规则)
我的当前想法是,只有在成员字段的Tsealed时,才使用IEquatable<T>生成的比较函数。如果T未封闭,即使T实现了IEquatable<T>,也应该使用虚拟的Object.Equals(Object obj),因为该字段可能存储T的子类型,而我认为大多数IEquatable<T>的实现并没有为继承而设计。
4个回答

25

我一直在思考这个问题,经过一番考虑,我同意只有在密封类型上实现IEquatable<T>IComparable<T>的做法。

我来回思考了一会儿,但是接下来我想到了以下的测试。在什么情况下以下内容应该返回false呢?在我看来,两个对象要么相等,要么不相等。

public void EqualitySanityCheck<T>(T left, T right) where T : IEquatable<T> {
  var equals1 = left.Equals(right);
  var equals2 = ((IEqutable<T>)left).Equals(right);
  Assert.AreEqual(equals1,equals2);
}

给定对象上的IEquatable<T> 的结果应该与Object.Equals 具有相同的行为,假设比较器是同等类型。在对象层次结构中两次实现IEquatable<T> 可以允许,并意味着,在您的系统中有两种不同的表达相等的方式。易于编造任何数量的情况,其中IEquatable<T>Object.Equals 将有所不同,因为有多个IEquatable<T> 实现,但只有一个Object.Equals。因此,上述方法将失败并在代码中产生一些混淆。
有人可能会认为在对象层次结构的较高点实现IEquatable<T> 是有效的,因为您想要比较对象的子集属性。在这种情况下,您应该优先考虑专门设计用于比较这些属性的IEqualityComparer<T>

2
完全正确,Jared。我想补充一下你所说的:如果要以继承安全的方式尝试实现IEquatable<T>,则必须提供一个虚拟的bool IsEqual(T obj)方法,并要求每次被子类型覆盖时,子类型的重写必须首先调用base.IsEqual(T obj),如果基础方法返回false,则返回false。当然,看看这个,问问自己有了虚拟object.Equals(object obj)的存在,也许你只需要使用它。第一个子类型会产生额外的转换开销,但其他后代必须进行转换。 - Phil
1
我越看,对于非密封类实现IEquatable<T>而不是object.Equals越觉得没有意义。当然,对于值类型,它们自动被密封,可以避免装箱和转换,因此性能提高了。对于类而言,唯一的性能提高就是避免转换,但如果你没有将类密封,那么它应该被设计为具有继承安全性,并且你最终可能会在子类上进行转换。然而,针对非密封类实现IEquatable<T>最大的争议在于(我认为)隐含契约是T的比较,而不是其子类型。 - Phil
1
@Phil:如果IEquatable<T>包括GetHashCode,那么定义为比较类型T的事物可能会很有用,而不是作为它们自己的类型。然而,由于它没有,因此IEquatable<T>与Object.Equals相差不大。顺便说一句,我希望Microsoft包括一个IIgnoreEquatable接口(没有成员),并使EqualityComparer.Default忽略实现IIgnoreEquatable的类的任何IEquatable接口。这样的东西将允许设计安全可继承的不可变类。 - supercat
1
@JeppeStigNielsen:这还不够。如果BaseObject:IEquatable<BaseObject>,而DerivedObject:BaseObject有一些额外的属性,确保两个仅在该额外属性上不同的DerivedObject实例互相识别为不同的唯一方法是让IEquatable<BaseObject>链接到一个虚拟方法(通常是Equals),然后必须将对象转换为派生类型。 - supercat
@JeppeStigNielsen:可以使用封装的 Equals 方法,在调用虚拟属性测试其他属性之前检查基类属性;这种设计在使用 IEquatable<T> 时是安全的,但即使是 IEquatable<T>.Equals 也需要检查对象的类型,与使用 Equals(Object) 相比并没有太多优势。 - supercat
显示剩余5条评论

5
通常不建议在任何非密封类上实现IEquatable<T>,或者在大多数情况下实现非泛型IComparable,但对于IComparable<T>则不能这样说。两个原因:
1.已经存在一种比较可能相同类型的对象的方法:Object.Equals。由于IEquatable<T>不包括GetHashCode,其行为必须与Object.Equals匹配。实现IEquatable<T>的唯一原因是性能。当应用于密封类类型时,IEquatable<T>相对于Object.Equals提供了轻微的性能改进,并且当应用于结构类型时提供了巨大的改进。然而,未密封类型的IEquatable<T>.Equals实现要确保其行为与可能被覆盖的Object.Equals相匹配,唯一的方法就是调用Object.Equals。如果IEquatable<T>.Equals必须调用Object.Equals,则任何可能的性能优势都会消失。
2.有时,基类具有涉及仅基类属性的定义自然排序的可能、有意义和有用的特性,这将在所有子类中保持一致。在检查两个对象是否相等时,结果不应取决于是否将对象视为基类型或派生类型。然而,在排名对象时,结果通常应该取决于用作比较基础的类型。派生类对象应实现IComparable<TheirOwnType>,但不应覆盖基类型的比较方法。当作为父类型进行比较时,两个派生类对象可以合理地比较为“未排序”,但当作为派生类型进行比较时,一个对象可以比另一个对象排名高。
在可继承的类中实现非泛型IComparable可能比实现IComparable<T>更有问题。最好的做法可能是允许基类实现它,如果不希望任何子类需要其他排序,则不要重新实现或覆盖父类实现。

我知道这是一个老话题,但仍然:我不认为你的第一点完全正确。object.Equals检查引用相等性,所以在我看来,实现IEquatable<T>的主要原因是改变该行为并检查值相等性(或者对于某种类型定义的相等性)。至于性能:我没有证据,但我非常确定object.Equals击败了大多数自制实现,因为它只检查引用相等性(当然只针对引用类型)。 - enzi
@enzi:静态的Object.Equals方法只在至少有一个参数为null时执行引用相等性检查。否则,它调用第一个对象对Object.Equals的重写。 Object.Equals的该重写和IEquatable<T>.Equals的实现(如果有)都应与GetHashCode()一致,这意味着除非GetHashCode()返回一个常量,否则两个Equals方法必须返回一致的结果。如果任何一个Equals方法测试引用相等性,则两个方法都必须如此。 - supercat
要明确的是,代码应该期望能够使用该覆盖来比较任意类型的内容,而不是寻找任何其他“Equals”方法。 - supercat
我指的是Object.Equals的默认实现。如果您重写它并实现了IEquatable<T>.Equals,那么两者应该返回相同的结果,并与GetHashCode一致。我只是指出,您首先要覆盖Object.Equals的原因不是性能,而是更改检查引用相等性的默认行为。对我来说,在您谈论IEquatable.Equals的性能优势时,您的回答中是否涉及重写的Object.Equals并不清楚。 - enzi
无论如何,我相信IEquatable<T>比(对于引用类型)微不足道的性能提升更有意义。通过在集合等中使用此接口,我可以保证不会出现运行时强制转换异常,因为某人试图将我的“Apple”与“Orange”进行比较。这只是对接口一般目的的备注,与是否在非密封类中实现它无关(我不会这样做)。 - enzi
显示剩余2条评论

1

我看到的大多数Equals实现都会检查被比较对象的类型,如果它们不同,则该方法返回false。

这样可以很好地避免子类型与其父类型进行比较的问题,从而避免了需要封闭类的需要。

一个明显的例子是尝试将2D点(A)与3D点(B)进行比较:对于2D点,3D点的x和y值可能相等,但对于3D点,z值很可能不同。

这意味着A == B将为true,但B == A将为false。大多数人喜欢Equals运算符是可交换的,因此在这种情况下检查类型显然是一个好主意。

但是,如果您创建了子类并且没有添加任何新属性怎么办?嗯,这有点难以回答,可能取决于您的情况。


ninj,我不确定我理解你的意思,仅检查其他对象的类型是不够的——如果在类型B上调用Equals(A obj)呢?例如,当你有一个List<A>时,完全有可能包含A和子类型B。而且,由于你没有封闭A,所以你无法知道在任何子类型(包括B)上调用Equals(A obj)时会产生什么意义。你将被迫提供一个虚成员,以便子类型可以根据需要覆盖行为。另一种选择可能是让Equals(A object)检查this.GetType() == typeof(A),如果为false则抛出异常。糟糕! - Phil
在我看来,IEquatable<T>的契约是比较类型为T的对象,而不是子类型。为了保证这一点,T需要被密封。如果你不同意这一点,想让T保持未密封状态,那么你需要设计IEquatable<T>实现以便子类型可以重写它,因此你必须引入一个额外的虚成员——但是考虑到System.Object已经有了虚拟的Object.Equals(Object obj),那么实现IEquatable<T>真的有任何意义吗? - Phil
哦,糟糕。好的观点,IEquatable<T> 实际上不应该处理检查类型(是的,这很棘手,必须这样做)。我之前正在实现一个 Equals 和 IEquatable 的帮助类,最终我采取了懦弱的方式 - 我将 IEquatable<T> 的目的从“类型安全相等”缩小到“使泛型字典之类的东西工作的原因”。如果你采取这种方式,就没有必要封闭类型... - ninj
1
@Phil 正确的做法是检查 this.GetType() == other.GetType(),而不是 this.GetType() == typeof(A)。如果第一个要求未满足,则返回 false,不要抛出异常!永远不要让两个实例被认为相等,如果其中一个比另一个更派生(你不知道 thisother 哪个更派生)。正如你(Phil)自己所说,“你无法知道对于 A 的任何子类型来说什么是有意义的”。 - Jeppe Stig Nielsen

0

今天在阅读时,我偶然发现了这个主题:
https://blog.mischel.com/2013/01/05/inheritance-and-iequatable-do-not-mix/
我同意,不实现 IEquatable<T> 是有原因的,因为存在错误实现的可能性。

然而,在阅读了上述链接文章后,我测试了自己在各种非密封、继承类中使用的实现,并发现它是正确的。
在实现 IEquatable<T> 时,我参考了这篇文章:
http://www.loganfranken.com/blog/687/overriding-equals-in-c-part-1/
它很好地解释了在 Equals() 中要使用的代码。虽然它没有涉及继承,但我自己调整了一下。这是结果。

回答原问题:
我不是说它必须在非密封类上实现,但我可以肯定地说,它可以毫无问题地实现。

//============================================================================
class CBase : IEquatable<CBase>
{
  private int m_iBaseValue = 0;

  //--------------------------------------------------------------------------
  public CBase (int i_iBaseValue)
  {
    m_iBaseValue = i_iBaseValue;
  }

  //--------------------------------------------------------------------------
  public sealed override bool Equals (object i_value)
  {
    if (ReferenceEquals (null, i_value))
      return false;
    if (ReferenceEquals (this, i_value))
      return true;

    if (i_value.GetType () != GetType ())
      return false;

    return Equals_EXEC ((CBase)i_value);
  }

  //--------------------------------------------------------------------------
  public bool Equals (CBase i_value)
  {
    if (ReferenceEquals (null, i_value))
      return false;
    if (ReferenceEquals (this, i_value))
      return true;

    if (i_value.GetType () != GetType ())
      return false;

    return Equals_EXEC (i_value);
  }

  //--------------------------------------------------------------------------
  protected virtual bool Equals_EXEC (CBase i_oValue)
  {
    return i_oValue.m_iBaseValue == m_iBaseValue;
  }
}

//============================================================================
class CDerived : CBase, IEquatable<CDerived>
{
  public int m_iDerivedValue = 0;

  //--------------------------------------------------------------------------
  public CDerived (int i_iBaseValue,
                  int i_iDerivedValue)
  : base (i_iBaseValue)
  {
    m_iDerivedValue = i_iDerivedValue;
  }

  //--------------------------------------------------------------------------
  public bool Equals (CDerived i_value)
  {
    if (ReferenceEquals (null, i_value))
      return false;
    if (ReferenceEquals (this, i_value))
      return true;

    if (i_value.GetType () != GetType ())
      return false;

    return Equals_EXEC (i_value);
  }

  //--------------------------------------------------------------------------
  protected override bool Equals_EXEC (CBase i_oValue)
  {
    CDerived oValue = i_oValue as CDerived;
    return base.Equals_EXEC (i_oValue)
        && oValue.m_iDerivedValue == m_iDerivedValue;
  }
}

测试:

private static void Main (string[] args)
{
// Test with Foo and Fooby for verification of the problem.
  // definition of Foo and Fooby copied from
  // https://blog.mischel.com/2013/01/05/inheritance-and-iequatable-do-not-mix/
  // and not added in this post
  var fooby1 = new Fooby (0, "hello");
  var fooby2 = new Fooby (0, "goodbye");
  Foo foo1 = fooby1;
  Foo foo2 = fooby2;

// all false, as expected
  bool bEqualFooby12a = fooby1.Equals (fooby2);
  bool bEqualFooby12b = fooby2.Equals (fooby1);
  bool bEqualFooby12c = object.Equals (fooby1, fooby2);
  bool bEqualFooby12d = object.Equals (fooby2, fooby1);

// 2 true (wrong), 2 false
  bool bEqualFoo12a = foo1.Equals (foo2);  // unexpectedly "true": wrong result, because "wrong" overload is called!
  bool bEqualFoo12b = foo2.Equals (foo1);  // unexpectedly "true": wrong result, because "wrong" overload is called!
  bool bEqualFoo12c = object.Equals (foo1, foo2);
  bool bEqualFoo12d = object.Equals (foo2, foo1);

// own test
  CBase oB = new CBase (1);
  CDerived oD1 = new CDerived (1, 2);
  CDerived oD2 = new CDerived (1, 2);
  CDerived oD3 = new CDerived (1, 3);
  CDerived oD4 = new CDerived (2, 2);

  CBase oB1 = oD1;
  CBase oB2 = oD2;
  CBase oB3 = oD3;
  CBase oB4 = oD4;

// all false, as expected
  bool bEqualBD1a = object.Equals (oB, oD1);
  bool bEqualBD1b = object.Equals (oD1, oB);
  bool bEqualBD1c = oB.Equals (oD1);
  bool bEqualBD1d = oD1.Equals (oB);

// all true, as expected
  bool bEqualD12a = object.Equals (oD1, oD2);
  bool bEqualD12b = object.Equals (oD2, oD1);
  bool bEqualD12c = oD1.Equals (oD2);
  bool bEqualD12d = oD2.Equals (oD1);
  bool bEqualB12a = object.Equals (oB1, oB2);
  bool bEqualB12b = object.Equals (oB2, oB1);
  bool bEqualB12c = oB1.Equals (oB2);
  bool bEqualB12d = oB2.Equals (oB1);

// all false, as expected
  bool bEqualD13a = object.Equals (oD1, oD3);
  bool bEqualD13b = object.Equals (oD3, oD1);
  bool bEqualD13c = oD1.Equals (oD3);
  bool bEqualD13d = oD3.Equals (oD1);
  bool bEqualB13a = object.Equals (oB1, oB3);
  bool bEqualB13b = object.Equals (oB3, oB1);
  bool bEqualB13c = oB1.Equals (oB3);
  bool bEqualB13d = oB3.Equals (oB1);

// all false, as expected
  bool bEqualD14a = object.Equals (oD1, oD4);
  bool bEqualD14b = object.Equals (oD4, oD1);
  bool bEqualD14c = oD1.Equals (oD4);
  bool bEqualD14d = oD4.Equals (oD1);
  bool bEqualB14a = object.Equals (oB1, oB4);
  bool bEqualB14b = object.Equals (oB4, oB1);
  bool bEqualB14c = oB1.Equals (oB4);
  bool bEqualB14d = oB4.Equals (oB1);
}

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