繁体   English   中英

IEquatable <T>,IComparable <T>应该在非密封类上实现吗?

[英]Should IEquatable<T>, IComparable<T> be implemented on non-sealed classes?

任何人对IEquatable<T>IComparable<T>是否应该通常要求Tsealed (如果它是一个class )有任何意见?

这个问题发生在我身上,因为我正在编写一组旨在帮助实现不可变类的基类。 基类要提供的部分功能是自动实现相等比较(使用类的字段以及可应用于字段来控制相等比较的属性)。 当我完成它时应该非常好 - 我正在使用表达式树为每个T动态创建一个编译的比较函数,因此比较函数应该非常接近正则相等比较函数的性能。 (我正在使用一个键入System.Type的不可变字典和双重检查锁定来以合理的方式存储生成的比较函数)

尽管如此,有一件事是用来检查成员字段相等性的函数。 我的初衷是检查每个成员字段的类型(我称之为X )是否实现了IEquatable<X> 然而,经过一番思考后,除非Xsealed否则我认为这是不安全的。 原因是如果X没有被sealed ,我无法确定X是否正确地将等式检查委托给X上的虚方法,从而允许子类型覆盖相等比较。

这就提出了一个更普遍的问题 - 如果一个类型没有被密封,它是否应该真正实现这些接口? 我想不会,因为我认为接口契约是比较两种X类型,而不是两种类型,可能是也可能不是X (虽然它们当然必须是X或子类型)。

你们有什么感想? 是否应该为未密封的类避免使用IEquatable<T>IComparable<T> (也让我想知道是否有一个fxcop规则)

我目前的想法是让我生成的比较函数仅IEquatable<T> sealed T成员字段使用IEquatable<T> ,而是使用虚拟Object.Equals(Object obj)如果T未启封,即使T实现了IEquatable<T> ,因为该字段可能存储T子类型,我怀疑IEquatable<T>大多数实现是否适合继承。

我一直在考虑这个问题,经过一些考虑,我同意实现IEquatable<T>IComparable<T>只能在密封类型上完成。

我来回走了一会儿然后我想到了下面的测试。 在什么情况下,以下应该返回虚假? 恕我直言,2个对象要么相等,要么不对。

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>Object.Equals会有所不同。 因此,上述操作会失败并在您的代码中造成一些混乱。

有些人可能会争辩说,在对象层次结构中的更高点实现IEquatable<T>是有效的,因为您想要比较对象属性的子集。 在这种情况下,您应该支持IEqualityComparer<T> ,它专门用于比较这些属性。

我通常建议不要在任何非密封类上实现IEquatable <T>,或者在大多数情况下实现非通用IComparable,但对于IComparable <T>则不能这样说。 两个原因:

  1. 已经存在一种比较可能是或可能不是同一类型的对象的方法:Object.Equals。 由于IEquatable <T>不包含GetHashCode,因此其行为必须与Object.Equals的行为相匹配。 除了Object.Equals之外,实现IEquatable <T>的唯一原因是性能。 与适用于密封类类型的Object.Equals相比,IEquatable <T>提供了较小的性能提升,并且在应用于结构类型时有了很大的改进。 未密封类型的IEquatable <T> .Equals实现的唯一方法是确保其行为与可能被覆盖的Object.Equals的行为匹配,但是,调用Object.Equals。 如果IEquatable <T> .Equals必须调用Object.Equals,任何可能的性能优势都会消失。
  2. 对于基类来说,有时可能,有意义且有用,它具有仅涉及基类属性的已定义自然顺序,这些属性将通过所有子类保持一致。 检查两个对象是否相等时,结果不应取决于是将对象视为基类型还是派生类型。 但是,在对对象进行排名时,结果通常取决于用作比较基础的类型。 派生类对象应实现IComparable <TheirOwnType>,但不应覆盖基类型的比较方法。 当作为父类型进行比较时,两个派生类对象比较为“未分配”是完全合理的,但是当作为派生类型进行比较时,要比较另一个对象。

在可继承类中实现非泛型IComparable可能比IComparable <T>的实现更值得怀疑。 可能最好的做法是允许基类实现它,如果不期望任何子类需要一些其他排序,但子类不重新实现或覆盖父类实现。

我见过的大多数Equals实现检查被比较的对象的类型,如果它们不相同则方法返回false。

这巧妙地避免了子类型与其父类型进行比较的问题,从而无需密封类。

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

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

但是如果你是子类并且你没有添加任何新属性呢? 嗯,这有点难以回答,可能取决于你的情况。

我今天在阅读时偶然发现了这个话题
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);
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM