【问题标题】:MS Guidelines for Overriding Equals() fails own criteria. Best practice to use instead?覆盖 Equals() 的 MS 指南不符合自己的标准。使用最佳实践?
【发布时间】:2016-08-07 11:47:42
【问题描述】:

我正在查看 Microsoft 为覆盖 Equals 运算符而发布的指南。 https://msdn.microsoft.com/en-us/library/ms173147(v=vs.90).aspx

他们声明:

Equals 的新实现应该遵循 等于:

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

他们随后提供了基类和子类(TwoDPoint 和 ThreeDPoint,代码如下)的示例,它们实现了覆盖此方法的最佳实践。

但是,这两个示例类未能满足刚刚给出的“等式保证”。 IE,TwoDPoint.Equals(ThreeDPoint) 可以返回 true,但 ThreeDPoint.Equals(TwoDPoint) 将始终返回 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 也检查两个对象的类型是否相同?浏览器

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

真的,我想归结为,如果所有基类字段都匹配,那么基类 equals() 方法为子类返回 true 是否被认为是“可以的”?从基类的角度来看,这是有道理的,恕我直言,但是,您最终确实违反了上面的规则 #2。打破该规则会对字典和哈希集等对象产生什么影响。您只是要求一些细微的错误吗?

【问题讨论】:

  • 我已经看到了两种方式的实现。答案是视情况而定。
  • 你不应该仅仅为了代码重用的目的而使用继承。
  • 有人在文章cmets中指出了这一点,团队似乎已经回复This topic is no longer in the current library, and it was replaced long enough ago that I can no longer update it.

标签: c# equality


【解决方案1】:

这些例子很糟糕。您会在 MSDN 中经常看到这一点 - 他们通常只关注一件事,而忽略其他所有内容。

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

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

值相等要复杂得多,更重要的是,相等和身份之间存在一些重叠。

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

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

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

当您查看 MSDN 用于此示例的类型时,您会发现这个概念是多么荒谬。那些首先不应该是子类!任何 3D 点都不可能等于 2D 点,3D 点永远不能替代 2D 点。这不仅违反了 Equals 准则,还违反了常见的对象设计实践。它使用继承来重用代码,这不是对象设计的好方法。子类应该始终是其祖先类的有效替代品,这显然不是这里的情况。

人们会犯错误。编写 MSDN 的人也这样做。您会发现 .NET BCL 有很多地方是错误的——也许它们在某一时刻是有意义的,也许实践发生了变化,或者设计它们的人没有把它弄好。这种情况一直在发生,你必须为此做好准备。实践不是一成不变的,它们总是与上下文相关 - 您必须了解推理,以便您可以选择它们对于给定场景是否有意义。现在问问你自己,你想让你的 hashset 类认为 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&lt;T&gt; 的方式实现IComparable&lt;T&gt; 几乎没有什么不同——它只是看起来“不同”,因为接口是隐式的。但它仍然是您必须遵守的接口。仅仅因为它们具有与您想要的相似的方法签名而重用接口是很糟糕的。而且我过去也犯过这样的错误——只要需要void () 代表就使用ThreadStart 代表(另一方面,Action 非常好——你没有违反“做一些不返回任何参数且不带参数的操作”)。

【讨论】:

    【解决方案2】:

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

    如果 2dpoint1.Equals(2dpoint2) 则 2dpoint2.Equals(2dpoint1) 也必须相等。并且 3dpoint1.Equals(3dpoint2) 那么 3dpoint2.Equals(3dpoint1) 也必须相等。

    所以应该是,“当比较同一类的 2 个实例时......”

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

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

    【讨论】:

      【解决方案3】:

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

      话虽如此,您需要考虑在您自己的代码中继承模型是否真的有意义,以及比较两个实例的位置。在示例情况下,可能另一段代码(如断言)在比较 2D 和 3D 对象是否相等时应该引发异常。

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

      【讨论】:

      • 我认为您在现实生活中的例子具有误导性——当然,我们所说的“平等”是主观的。但是object.Equals 方法的含义不是主观的——它是许多代码所依赖的已定义合约。如果你需要一种不同的平等……好吧,那就去做你自己的方法吧。其余的答案是正确的:)
      • 合同没有那么严格。默认的 Equals 是对象的引用相等,但在这种情况下,我们会突然覆盖该行为,说两个完全不同的实例相等,因为它们在 2D 空间中的坐标相同。对我来说,这是对平等概念的更大曲解,而不仅仅是添加更多属性进行检查。
      猜你喜欢
      • 1970-01-01
      • 2013-08-09
      • 1970-01-01
      • 2011-06-09
      • 1970-01-01
      • 2011-11-05
      • 2011-02-11
      • 1970-01-01
      • 2010-09-20
      相关资源
      最近更新 更多