你觉得这很令人困惑是完全正确的。真是一团糟。
让我们首先通过查看更多示例清楚地说明会发生什么,然后我们将推断出此处应用的正确规则。让我们扩展您的程序以考虑所有这些情况:
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));
所有这些都打印 True,除了我标注为打印 false 的那些。
我现在将对这些案例进行分析。
首先要注意的是== 运算符总是说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 都等于 double 2.0,因为 int 2 被转换为 double 2.0,并且比较为真。
可空==的语义是:
- 如果两个操作数都为 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是根据left操作数(接收方)的运行时类型来解析的,但是编译时类型的正确的操作数(参数),这就是事情出轨的原因。
让我们先来看看double.Equals(object) 做了什么。如果对Equals(object) 的调用的接收者是double,那么如果参数不是装箱的双精度,则认为它们不相等。也就是说,Equals 要求类型匹配,而== 要求类型可转换为通用类型。
我再说一遍。 double.Equals 确实不 尝试将其参数转换为双精度,这与== 不同。它只是检查它是否已经是双倍,如果不是,那么它说它们不相等。
这就解释了为什么d.Equals(i) 是假的......但是......等一下,上面的不是假的!这是什么原因?
double.Equals 已超载!上面我们实际上调用了double.Equals(double),它——你猜对了——在调用之前将int转换为double!如果我们说d.Equals((object)i)),那将是错误的。
好的,所以我们知道为什么double.Equals(int) 为真——因为 int 被转换为 double。
我们也知道为什么double.Equals(int?) 是假的。 int? 不能转换为 double,但可以转换为 object。所以我们调用double.Equals(object) 并将int 框起来,现在它不相等。
nd.Equals(object) 呢?其语义是:
- 如果接收者为空且参数为空,则它们相等
- 如果接收者不为空,则遵循
d.Equals(object) 的不可空语义
所以现在我们知道为什么 x 是 double 或 double? 时 nd.Equals(x) 有效,但如果它是 int 或 int? 则无效。 (虽然有趣的是,(default(double?)).Equals(default(int?)) 当然是真的,因为它们都是空的!)
最后,通过类似的逻辑,我们看到了为什么int.Equals(object) 给出了它所具有的行为。它检查它的参数是否是一个装箱的 int,如果不是,则返回 false。因此i.Equals(d) 是错误的。 i 不能转为 double,d 不能转为 int。
这是一团糟。我们希望 equality 是一个 等价关系,但事实并非如此!相等关系应具有以下属性:
- 自反性:事物与自身相等。这通常在 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
BONUS EXERCISE:重复上述分析,但对于 x?.Equals(y) 的情况,x 可以为空。什么时候得到与不可为空的接收者相同的结果,什么时候得到不同的结果?