MS Override Equals()准则不符合自己的标准.使用最佳实践

本文关键字:标准 最佳 自己的 不符合 Equals Override MS | 更新日期: 2024-09-08 08:44:33

我正在查看Microsoft发布的重写Equals运算符的指导方针。https://msdn.microsoft.com/en-us/library/ms173147(v=vs.90).aspx

他们声明:

Equals的新实施应遵循等于:

  • x.Equals(x)返回true
  • x.Equals(y)返回的值与y.Equal(x)相同
  • 如果(x.Equals(y)&y.Equals(z))返回true,然后x.Equals)返回true
  • 只要x和y引用的对象没有被修改,x.Equals(y)的连续调用就会返回相同的值
  • x.Equals(null)返回false(仅适用于不可为null的值类型。)

接下来,他们给出了基类和子类(TwoDPoint和ThreeDPoint,下面的代码)的示例,它们实现了覆盖此方法的最佳实践。

但是,这两个示例类不能满足刚刚给出的"等于的保证"。IE,TwoDPoint.Equals(ThreeDPoint)可以返回true,但ThreeDPoint.Equals将始终返回false。这使上面的第二个要点失败。

    static void Main(string[] args)
    {
        TwoDPoint twoDPoint = new TwoDPoint(1, 2);
        ThreeDPoint threeDPoint = new ThreeDPoint(1, 2, 3);
        //this will assert because twoDPoint.Equals(threeDPoint) == true 
        //but, threeDPoint.Equals(twoDPoint) == false
        AssertMicrosoftEqualsGuidelines(twoDPoint, threeDPoint, null);
    }
    /// <summary>
    /// Will Assert() if any of microsofts rules for Equals overriding fail.
    /// NOTE, x and y can not be null.
    /// https://msdn.microsoft.com/en-us/library/ms173147(v=vs.90).aspx
    /// </summary>
    static void AssertMicrosoftEqualsGuidelines(object x, object y, object z)
    {
        System.Diagnostics.Debug.Assert(x.Equals(x), "FAILED x.Equals(x) returns true.");
        System.Diagnostics.Debug.Assert(x.Equals(y) == y.Equals(x), "FAILED x.Equals(y) returns the same value as y.Equals(x).");
        if(x.Equals(y) && y.Equals(z))
        {
            System.Diagnostics.Debug.Assert(x.Equals(z), "FAILED Successive invocations of x. Equals (y) return the same value as long as the objects referenced by x and y are not modified.");
        }
        System.Diagnostics.Debug.Assert(x.Equals(y) == x.Equals(y) == x.Equals(y) == x.Equals(y), "Successive invocations of x. Equals (y) return the same value as long as the objects referenced by x and y are not modified.");
        System.Diagnostics.Debug.Assert(x.Equals(null) == false, "x.Equals (null) returns false");
    }
}
class TwoDPoint : System.Object
{
    public readonly int x, y;
    public TwoDPoint(int x, int y)  //constructor
    {
        this.x = x;
        this.y = y;
    }
    public override bool Equals(System.Object obj)
    {
        // If parameter is null return false.
        if (obj == null)
        {
            return false;
        }
        // If parameter cannot be cast to Point return false.
        TwoDPoint p = obj as TwoDPoint;
        if ((System.Object)p == null)
        {
            return false;
        }
        // Return true if the fields match:
        return (x == p.x) && (y == p.y);
    }
    public bool Equals(TwoDPoint p)
    {
        // If parameter is null return false:
        if ((object)p == null)
        {
            return false;
        }
        // Return true if the fields match:
        return (x == p.x) && (y == p.y);
    }
    public override int GetHashCode()
    {
        return x ^ y;
    }
}
class ThreeDPoint : TwoDPoint
{
    public readonly int z;
    public ThreeDPoint(int x, int y, int z)
        : base(x, y)
    {
        this.z = z;
    }
    public override bool Equals(System.Object obj)
    {
        // If parameter cannot be cast to ThreeDPoint return false:
        ThreeDPoint p = obj as ThreeDPoint;
        if ((object)p == null)
        {
            return false;
        }
        // Return true if the fields match:
        return base.Equals(obj) && z == p.z;
    }
    public bool Equals(ThreeDPoint p)
    {
        // Return true if the fields match:
        return base.Equals((TwoDPoint)p) && z == p.z;
    }
    public override int GetHashCode()
    {
        return base.GetHashCode() ^ z;
    }
}

那么,"平等保障"的指导方针是错误的吗?重写equals的人是否也应该检查这两个对象的类型是否相同?IE

if(GetType() != obj.GetType()){return false;} //include in Equals()?

真的,我想归根结底,如果所有基类字段都匹配,那么让基类equals()方法为子类返回true是否被认为是"可以的"?从基类的角度来看,这是有道理的,但是,您最终会破坏上面的规则#2。打破这一规则会对字典和哈希集等项目产生什么影响?你会只是在问一些微妙的错误吗?

MS Override Equals()准则不符合自己的标准.使用最佳实践

这些例子很糟糕。你会在MSDN中经常看到这种情况——它们通常只关注一件事,而忽略其他一切。

相等是一个棘手的概念,它不适合继承(你会经常看到"不适合继承"——继承很…棘手)。如前所述,关于平等有两种主要的思考方式——价值平等和参考平等。有趣的是,两者可能重叠,也可能不重叠。

引用相等是比较简单的一种。它非常适合继承,因为它不会对您正在比较的对象进行任何解释——要么引用相同,要么不相同。所有指南都适用于参考平等。

价值平等要复杂得多,更重要的是,平等和身份之间有一些重叠。

谈到严格的价值平等,它只是不适用于继承。使用structs实现严格的值相等非常容易,尤其是如果您遵循了结构设计的所有准则。没有继承,理想情况下,您的结构是不可变的。这同样适用于匿名类型——这就是为什么它们可以在默认情况下实现值相等;局限性使它变得相当简单。

如果两个对象不属于同一类型,那么它们就不能严格相等。因此,实际上,为了获得最佳效果,您永远不应该允许myClass.Equals(subclass)或其他方式。这对于依赖于Equals的正确行为的其他代码(例如哈希集)来说非常重要。

由于在某些情况下,除了严格的平等之外,做其他事情是可行的,人们写了许多不同的比较方法。也许你关心对象的ID,但不关心其他字段——身份。也许您想看看是否需要更新数据库中的对象。有些人重写Equals方法来提供此功能,这完全是错误的。如果你有这样的问题,制定你自己的方法。这不像。NET限制了一个类可以有多少方法或接口:)

当您看到MSDN在这个例子中使用的类型时,您可以看到这个概念是多么荒谬。首先,这些不应该是子类!任何3D点都不可能等于2D点,3D点也不可能是2D点的替换。这不仅违反了Equals准则,还违反了常见的对象设计实践。它使用继承来实现代码重用,这不是一种很好的对象设计方法。子类应该始终是其祖先类的有效替换,但这里显然不是这样。

人们会犯错。那些写MSDN的人也这样做。你会发现的。NET BCL有很多错误之处——也许它们在某一点上是有意义的,也许实践发生了变化,或者设计它们的人没有把它做好。这种情况经常发生,你必须为此做好准备。实践不是板上钉钉的,它们总是非常有背景的——你必须理解其中的原因,这样你才能选择它们对给定的场景是否有意义。现在问问自己,你想让你的哈希集类认为2D点等于3D点吗?想象一下这样的代码:

var set = new HashSet<2DPoint>();
set.Add(new 2DPoint(3, 3));
set.Add(new 3DPoint(3, 3, 0));

第二个Add应该失败吗?如果这是一种添加或更新方法,而你认为你添加了一个3DPoint,但实际上你只是保持了"相等"的旧2DPoint,该怎么办?对于获得2DPoint而不是3DPoint,您的代码会有多高兴?

如果你需要任何不是严格值相等或引用相等的东西。。。只需添加您自己的方法。或者你自己的界面。但不要仅仅因为Equals已经存在就"重用"它——它的接口非常清晰,这违反了这一点。它与以不符合IComparable<T>的方式实现IComparable<T>没有什么不同——它只是看起来"不同",因为接口是隐式的。但它仍然是一个你必须遵守的接口。仅仅因为接口的方法签名与你想要的相似而重用接口是很糟糕的。我过去也犯过这种罪——每当需要void ()委托时都使用ThreadStart委托(另一方面,Action非常好——你没有违反"做一些不返回任何参数的操作"的接口)。

我认为这只是一个概念问题。不知怎的,他们通过继承2D点构建了一个3D点,这在逻辑上毫无意义。如果我们以基本值(双关语)认为3D点是转换为另一个维度的2D点,那么从二维的角度来看,无论3D点的高度如何,它都是相同的。

话虽如此,您需要考虑在您自己的代码中,继承模型是否真的有意义,以及您在哪里比较两个实例。在本例中,当比较2D和3D对象的相等性时,可能另一段代码(如断言)应该抛出异常。

让我给你举一个现实生活中的例子。你看到两辆相同型号的梅赛德斯-奔驰汽车,你会问自己"这些车是一样的吗?"答案是肯定的。然后你意识到其中一种颜色与另一种不同,你意识到答案是否定的。平等不是绝对的,这取决于你的观点。现在,这正式成为了一个哲学答案。

你看错了。当你说如果2dpoint.Equals(3dPoint)为真,那么3dPoint.Equaals(2dpoint)也必须为真时,你并不是在比较苹果和苹果。

如果2dpoint1.Equals(2dpoint2),则2dpoint2.Equals也必须相等。和3dpoint1.Equals(3dpoint2),然后是3dpoint2.equal(3dpoint1)也必须相等。

所以应该是,"当比较同一类的两个实例时…"

还要注意,在Microsoft示例中,您将无法使用my3DPoint.Equals(my2DPoint),因为在3D点类中没有采用2D点的Equals方法。

我们在处理3D数据的应用程序中使用了非常相似的类。我们确实使用my2DPoint.Equals(my3DPoint)来测试点是否在X,Y平面中对齐,但永远不会使用my3DPoint.Equaals(my2DPoint),因为这意味着我想要一个2D点无法进行的3D比较。