“double” 和 “int” 在 .Equals 比较中有什么区别?

8

我遇到了一个非常奇怪的情况,我不理解。以下是简化的情况:

double? d = 2;
int? i = 2;

Console.WriteLine(d.Equals((2))); // false
Console.WriteLine(i.Equals((2))); // true

我不明白为什么一个表达式会返回true,而另一个却是false。它们看起来一模一样。


它们在文本上看起来可能相同,但它们的二进制表示非常不同。有关更多信息,请参见此答案 - Frontear
2
这个问题不是关于如何比较双精度浮点数和整数的重复问题。问题在于将可空的双精度浮点数与整数进行比较会得到一个答案,而将相同的非可空双精度浮点数进行比较则会得到不同的答案。 - Eric Lippert
2
这个问题的踩票是没有道理的。问题很清楚,而且很明显不清楚C#的哪些规则导致了这种行为。这种行为是C#中一个极其糟糕的设计错误的例子;C#处理相等性的方式非常糟糕。 - Eric Lippert
如果另一个值不是int32,int32.equals将返回false。https://referencesource.microsoft.com/#mscorlib/system/int32.cs,225942ed7b7a3252 - pm100
2个回答

14

你完全正确,发现这个问题确实很令人困惑。这真是一团糟。

让我们通过更多的例子来清晰地说明发生了什么,然后我们将推导出正确的规则。让我们扩展您的程序以考虑所有这些情况:

    double d = 2;
    double? nd = d;
    int i = 2;
    int? ni = i;
    Console.WriteLine(d == d);
    Console.WriteLine(d == nd);
    Console.WriteLine(d == i);
    Console.WriteLine(d == ni);
    Console.WriteLine(nd == d);
    Console.WriteLine(nd == nd);
    Console.WriteLine(nd == i);
    Console.WriteLine(nd == ni);
    Console.WriteLine(i == d);
    Console.WriteLine(i == nd);
    Console.WriteLine(i == i);
    Console.WriteLine(i == ni);
    Console.WriteLine(ni == d);
    Console.WriteLine(ni == nd);
    Console.WriteLine(ni == i);
    Console.WriteLine(ni == ni);
    Console.WriteLine(d.Equals(d));
    Console.WriteLine(d.Equals(nd));
    Console.WriteLine(d.Equals(i));
    Console.WriteLine(d.Equals(ni)); // False
    Console.WriteLine(nd.Equals(d));
    Console.WriteLine(nd.Equals(nd));
    Console.WriteLine(nd.Equals(i)); // False
    Console.WriteLine(nd.Equals(ni)); // False
    Console.WriteLine(i.Equals(d)); // False
    Console.WriteLine(i.Equals(nd)); // False
    Console.WriteLine(i.Equals(i)); 
    Console.WriteLine(i.Equals(ni));
    Console.WriteLine(ni.Equals(d)); // False
    Console.WriteLine(ni.Equals(nd)); // False
    Console.WriteLine(ni.Equals(i)); 
    Console.WriteLine(ni.Equals(ni));

除了我标记为输出false的那些之外,所有这些都打印为True。

现在我将对这些情况进行分析。

首先要注意的是==运算符总是返回True。这是为什么呢?

非空==的语义如下:

int == int -- compare the integers
int == double -- convert the int to double, compare the doubles
double == int -- same
double == double -- compare the doubles

因此,在每种非可空情况下,整数2等于双精度2.0,因为int 2转换为double 2.0,并且比较结果为true。

可空类型 == 的语义如下:

  • 如果两个操作数都为null,则它们相等
  • 如果一个为null而另一个不是,则它们不相等
  • 如果两者都不为null,则回退到上面的非可空情况。

因此,对于可空比较, int? == double? int? == double 等,我们总是回退到非可空情况,将 int?转换为 double ,并在双精度中进行比较。 因此这些也都是正确的。

现在我们来看 Equals ,这就是事情变得混乱的地方。

这里有一个根本性的设计问题,我在2009年写过: https://blogs.msdn.microsoft.com/ericlippert/2009/04/09/double-your-dispatch-double-your-fun/ -- 问题在于 == 的意义是基于两个操作数的编译时类型确定的。但是, Equals 是基于左操作数(接收器)的运行时类型解析的,但是基于右操作数(参数)的编译时类型,这就是为什么事情会出错的原因。

让我们从看 double.Equals(object)开始。如果调用 Equals(object)的接收器是double,那么如果参数不是装箱的double,则认为它们不相等。也就是说,Equals要求类型匹配,而 == 要求类型可转换为公共类型

我再重复一遍。与 == 不同, double.Equals 并不尝试将其参数转换为double。 它只检查它是否已经是double,如果不是,则表示它们不相等。

那么这就解释了为什么d.Equals(i)是false...但是...等等,它在上面不是false!这是为什么呢?

double.Equals是重载的!上面,我们实际上正在调用double.Equals(double),你猜对了——在调用之前将int转换为double! 如果我们说d.Equals((object)i)),那么它将是false。

好的,我们知道为什么double.Equals(int)是true——因为int被转换为double。

我们也知道为什么double.Equals(int?)返回false。 int?不能转换为double,但可以转换为object。 因此,我们调用double.Equals(object)并装箱int,现在它不相等。

nd.Equals(object)呢? 它的语义是:

  • 如果接收者为null且参数为null,则它们相等
  • 如果接收者不为null,则转而使用d.Equals(object)的非可空语义

因此,现在我们知道nd.Equals(x)适用于xdoubledouble?,但如果是intint?则不适用。 (有趣的是,当然,(default(double?))。Equals(default(int?))将是true,因为它们都为null!)

最后,通过类似的逻辑,我们看到int.Equals(object)产生其具有的行为方式。 它检查其参数是否为包含的int,如果不是,则返回false。 因此,i.Equals(d)为false。 i不能转换为double,d不能转换为int。

这是一团糟。 我们希望等式成为一个等价关系,但它并不是! 等式关系应具有以下属性:

  • 自反性:一件事等于它本身。 尽管C#中有一些例外情况,但通常如此。
  • 对称性:如果A等于B,则B等于A。 在C#中的==是正确的,但在A.Equals(B)中不正确,如我们所见。
  • 传递性:如果A等于B且B等于C,则A也等于C。 在C#中绝对不是这种情况。

因此,在所有级别上都很混乱。 == Equals 具有不同的调度机制并给出不同的结果,它们都不是等价关系,并且一直令人困惑。 对于让您遇到这种情况,我表示歉意,但我来到这里时就已经很混乱了。

有关C#中等式恶劣的另一种看法,请参见我对后悔的语言决策清单中的第9项:http://www.informit.com/articles/article.aspx?p=2425867

奖励练习:针对可空接收者,重复上述分析,但为x?.Equals(y)的情况。 何时获得与非可空接收者相同的结果,何时获得不同的结果?


1
看起来答案在每个类型的Equals方法的源代码中。如果类型不匹配,则它们不相等。

https://referencesource.microsoft.com/#mscorlib/system/double.cs,147

// True if obj is another Double with the same value as the current instance.  This is
// a method of object equality, that only returns true if obj is also a double.
public override bool Equals(Object obj) {
    if (!(obj is Double)) {
        return false;
    }
    double temp = ((Double)obj).m_value;
    // This code below is written this way for performance reasons i.e the != and == check is intentional.
    if (temp == m_value) {
        return true;
    }
    return IsNaN(temp) && IsNaN(m_value);
}

https://referencesource.microsoft.com/#mscorlib/system/int32.cs,72

public override bool Equals(Object obj) {
    if (!(obj is Int32)) {
        return false;
    }
    return m_value == ((Int32)obj).m_value;
}

谢谢,这是一个非常清晰的答案。同时,也非常出乎意料。编辑:实际上我得收回之前的话,我不确定这个答案是否适用于可空类型。 - Iteria
@Iteria:这个答案已经接近完美了。你还需要知道的另外两件事是:(1) Nullable<T>.Equals(object) 检查是否为空,然后将其委托给基础类型 T 上的 object.Equals(object) 方法,(2) double.Equals() 有多种重载形式。 - Eric Lippert
1
啊,我明白了。在这个答案和你上面的答案之间。我想我懂了。 - Iteria

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