代码之家  ›  专栏  ›  技术社区  ›  Phil

是否应在非密封类上实现IEquatable<t>、IComparable<t>?

  •  35
  • Phil  · 技术社区  · 15 年前

    任何人对是否 IEquatable<T> IComparable<T> 一般要求 T sealed (如果是 class )?

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

    不过,出现了一个问题,那就是使用什么函数来检查成员字段的相等性。我的初衷是检查每个成员字段的类型(我将调用 X 工具 IEquatable<X> . 但是,经过一番考虑,我认为这是不安全的使用,除非 X 密封的 . 原因是如果 X 不是 密封的 我不确定是否 X 正在将相等性检查适当地委托给上的虚拟方法 X 从而允许子类型重写相等比较。

    这就带来了一个更一般的问题——如果一个类型不是密封的,那么它真的应该实现这些接口吗??我不这么认为,因为我认为接口契约是比较两个 X 类型,不是两种类型 X (尽管他们当然是 X 或子类型)。

    你们觉得怎么样?应该 IEquatable<T> i可比较<t> 要避免未密封的类?(这也让我怀疑是否有fxcop规则)

    我目前的想法是只使用生成的比较函数 IEquatable<T> 在成员字段上 T 密封的 ,而不是使用虚拟 Object.Equals(Object obj) 如果 T 即使是在 T 器具 IEquatable<T> ,因为字段可能存储 T 我怀疑 IEquatable<T> 是为继承而设计的。

    4 回复  |  直到 15 年前
        1
  •  16
  •   JaredPar    15 年前

    我已经考虑过这个问题,经过一番考虑,我同意 IEquatable<T> IComparable<T> 只能在密封类型上执行。

    我来回走了一会儿,但后来我想到了下面的测试。在什么情况下,下面的内容会返回错误?imho,两个对象要么相等,要么不相等。

    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> 等值对象 因为有多个 IEquatable<T> 实现,但只有一个 等值对象 . 因此,上述操作将失败,并在代码中造成一些混乱。

    有些人可能会认为 IEquatable<T> 在对象层次结构的较高点上有效,因为您要比较对象属性的子集。在这种情况下,你应该赞成 IEqualityComparer<T> 专门设计用于比较这些属性。

        2
  •  5
  •   supercat    13 年前

    我通常建议不要在任何非密封类上实现IEquatable<t>,或在大多数类上实现非泛型IComparable,但对于IComparable<t>,不能这样说。两个原因:

    1. 已经存在一种比较对象的方法,对象可能是同一类型,也可能不是同一类型:object.equals。由于IEquatable<t>不包含GetHashCode,因此其行为本质上必须与Object.Equals的行为匹配。除了object.equals之外,实现IEquatable<t>的唯一原因是性能。IEquatable<t>与Object.Equals相比,在应用于密封类类型时提供了较小的性能改进,在应用于结构类型时提供了较大的改进。未密封类型实现IEquatable<t>.Equals的唯一方法是确保其行为与可能被重写的对象的行为匹配。但是,Equals是调用Object.Equals。如果IEquatable<t>.Equals必须调用Object.Equals,则任何可能的性能优势都将消失。
    2. 对于一个基类,它有时是可能的、有意义的和有用的,因为它有一个定义的自然顺序,只涉及基类属性,而这些属性在所有子类中都是一致的。当检查两个对象是否相等时,结果不应该取决于是否将对象视为基类型或派生类型。然而,当对对象进行排序时,结果通常应取决于用作比较基础的类型。派生类对象应实现IComparable,但不应重写基类型的比较方法。当与父类型进行比较时,两个派生类对象比较为“未分级”是完全合理的,但当与派生类型进行比较时,一个对象比另一个对象更为合理。

    在可继承类中实现非泛型IComparable可能比实现IComparable<t>更令人怀疑。如果不期望任何子类都需要其他排序,那么最好的做法可能是允许基类实现它,但是对于子类,不需要重新实现或重写父类实现。

        3
  •  1
  •   ninj    15 年前

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

    这样可以巧妙地避免将子类型与父类型进行比较的问题,从而不需要密封类。

    一个明显的例子是试图比较二维点(a)和三维点(b):对于二维点,三维点的x和y值可能相等,但对于三维点,z值很可能不同。

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

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

        4
  •  0
  •   Tobias Knauss    6 年前

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