此行为在 C# 规范中定义(ECMA-334 http://www.ecma-international.org/publications/standards/Ecma-334.htm)在第 14.2.7 节中(我已突出显示相关部分):
对于关系运算符
< > <= >=
如果操作数类型均为不可空值类型并且结果类型,则存在运算符的提升形式
是bool
。提升形式是通过添加单个?
每个操作数类型的修饰符。被举起的
运算符产生值false
如果一个或两个操作数是null
。否则,提升的操作员会打开包装
操作数并应用底层运算符来生成bool
result.
特别是,这意味着通常的关系法则不成立;x >= y
并不意味着!(x < y)
.
血淋淋的细节
有些人问为什么编译器决定这是一个提升运算符int?
首先。我们来看一下。 :)
我们从 14.2.4“二元运算符重载解析”开始。这详细说明了要遵循的步骤。
首先,检查用户定义的运算符的适用性。这是通过检查由每一侧的类型定义的运算符来完成的>=
...这就提出了一个问题:null
是!这null
在给定类型之前,文字实际上没有任何类型,它只是“空文字”。通过遵循 14.2.5 中的指示,我们发现这里没有合适的运算符,因为 null 文字没有定义任何运算符。
此步骤指示我们检查预定义运算符集的适用性。 (本节也排除了枚举,因为双方都不是枚举类型。)相关的预定义运算符在第 14.9.1 到 14.9.3 节中列出,它们都是原始数字类型的运算符,以及这些运算符(请注意string
这里不包括 s 运算符)。
最后,我们必须使用这些运算符和 14.4.2 中的规则执行重载决策。
实际上执行这个解决方案会非常乏味,但幸运的是有一个捷径。第 14.2.6 节给出了重载决策结果的信息示例,其中指出:
...考虑二元 * 运算符的预定义实现:
int operator *(int x, int y);
uint operator *(uint x, uint y);
long operator *(long x, long y);
ulong operator *(ulong x, ulong y);
void operator *(long x, ulong y);
void operator *(ulong x, long y);
float operator *(float x, float y);
double operator *(double x, double y);
decimal operator *(decimal x, decimal y);
当重载解析规则(§14.4.2)应用于这组运算符时,效果是选择第一个
存在来自操作数类型的隐式转换的运算符。
由于双方都是null
我们可以立即扔掉所有未提升的操作员。这给我们留下了所有原始数字类型的提升数字运算符。
然后,使用前面的信息,我们选择第一个存在隐式转换的运算符。由于 null 文字可以隐式转换为可空类型,并且可空类型存在于int
,我们从列表中选择第一个运算符,即int? >= int?
.