为什么当没有定义 == 时,Nullable<T> 中的 == 运算符仍然有效?

16
我刚看了一下这篇答案,其中包含.NET反编译器中的Nullable<T>代码,并注意到两件事情:
  1. Nullable<T>T 时需要显式转换。
  2. ==运算符未定义。
鉴于这两个事实,令我惊奇的是,以下代码可以编译通过:
int? value = 10;
Assert.IsTrue(value == 10);

使用代码value == 10时,要么将value神奇地转换为int(从而允许使用int==操作符),要么神奇地为Nullable<int>定义==操作符。(或者,我认为不太可能的情况是Reflector省略了一些代码。)
我希望执行以下操作之一:
Assert.IsTrue((value.Equals(10)); // works because Equals *is* defined
Assert.IsTrue(value.Value == 10); // works because == is defined for int
Assert.IsTrue((int?)value == 10); // works because of the explicit conversion

当然,这些都能用,但==也能用,这就是我不理解的部分。

我注意到这一点并且提出这个问题是因为我正在尝试编写一个类似于Nullable<T>的结构体。我从上面链接的反射器代码开始,只做了一些非常小的修改。不幸的是,我的CustomNullable<T>没有以同样的方式工作。 我无法执行Assert.IsTrue(value == 10)。我会得到 "Operator == cannot be applied to operands of type CustomNullable<int> and int"。

现在,无论修改多小,我都不希望能够执行...

CustomNullable<T> value = null;

因为我了解Nullable<T>背后有一些编译器魔法,使得即使Nullable<T>是一个结构体,值也可以被设置为null,但我希望如果我的代码(几乎)完全相同,我应该能够模仿Nullable<T>的所有其他行为。

有人能解释一下当它们似乎没有定义时,Nullable<T>的各种运算符如何工作吗?


也许 Nullable 类重载了 == 运算符。也许这就是发生的事情? - Malk
1
好问题。现在问问自己这个问题:为什么你可以将int和可空int相加并得到可空int?Nullable<T>类没有定义加法运算符。 - Eric Lippert
@Eric,我本来打算尝试其他运算符的,但我觉得先发关于 == 的发现。无论如何,似乎 Nullable<T> 是一个“特权”结构体,编译器对其的处理方式与我自己编写的任何结构体都不同。我已经知道了允许将可空类型设置为 null 的魔法,但我想还有更多魔法。我的方向正确吗? - devuxer
2
@DanM:Nullable就像魔法一样。详细情况请看我的回答。我建议您在尝试模拟它们之前,要彻底熟悉运算符重载和nullable lifting的所有规则;这个规范读起来非常有趣。 - Eric Lippert
期望的工作代码第三行不应该是 Assert.IsTrue((int?)value == 10);,而是 Assert.IsTrue((int)value == 10);吗?正如之前所述,使用 (int?) 是一种惊喜,而不是期望。 - vyrp
5个回答

35

考虑到这两个事实,这编译通过令我感到惊讶。

只考虑这两个事实,这是令人惊讶的。

这里有第三个事实:C# 中,大多数运算符都被“提升到可空”的状态。

所谓“提升到可空”,就是说:

int? x = 1;
int? y = 2;
int? z = x + y;

如果 x 或 y 任意一个为 null,则 z 的值也为 null。如果两个都不为 null,则将它们的值相加,转换为可空类型,并将结果赋值给 z。

对于相等性也是如此,尽管在 C# 中,相等性有点奇怪,因为它仍然只有两个值。为了正确地支持可空类型,相等性应该是三个值:如果 x 或 y 中任意一个为 null,则 x == y 应该为 null,如果 x 和 y 都非 null,则为 true 或 false。这就是 VB 中的工作方式,但在 C# 中不是。

我希望我的代码如果写得(几乎)一样,我应该能够模仿所有其他 Nullable<T> 的行为。

你将不得不接受失望,因为你的期望与现实完全不符。 Nullable<T> 是一种非常特殊的类型,它的魔法属性深深嵌入在 C# 语言和运行时中。例如:

  • C# 自动将运算符提升为可空。没有办法说“自动将运算符提升到 MyNullable”。不过,通过编写自己的用户定义运算符,你可以接近这个目标。

  • C# 对 null 值有特殊的规则——你可以将其分配给可空变量,并将其与可空值进行比较,编译器会为它们生成特殊代码。

  • 可空类型的装箱语义非常奇怪,深深嵌入在运行时中。没有办法模拟它们。

  • isas 和合并运算符的可空语义深深嵌入到语言中。

  • 可空类型不满足 struct 约束。没有办法模拟它。

  • 等等。


感谢您的详细解释。有内部人员回答我们的问题真是太好了。我想我需要编写自己的运算符重载,但了解为什么我没有从(几乎)相同的代码中获得(几乎)等效的结果是很有帮助的。 - devuxer
一个 Nullable<ulong> 的赋值操作是否会因 C#/.NET 版本和目标架构的不同而意外地变成原子操作?我用 C# 5.0、Visual Studio 2013、.NET 4.5.2,以 x64 为目标架构编写了一个小型 WPF 程序,试图演示在普通的 ulong 和 Nullable<ulong> 上出现撕裂的情况。我很快就用普通的 ulong 找到了撕裂的情况,但是到目前为止,我还没有成功地在 Nullable<ulong> 上找到撕裂的情况。CLR 是否可能神奇地使赋值操作变成了原子操作呢? - Kal
@Eric 当你说“C#会自动将运算符提升为可空类型。没有办法说‘自动将运算符提升为MyNullable’。不过,你可以通过编写自己的用户定义运算符来实现相似的效果。”时,你认为为结构体定义接受可空类型的运算符是好还是坏的做法?例如,同时拥有public static bool operator <(MyStruct left, MyStruct right) => ...和public static bool operator <(MyStruct? left, MyStruct? right) => ...。 - Jarek Kardas
1
@JarekKardas:我想你误解了我的意思。当您在结构体“S”上提供用户定义的<时,编译器会为您免费提供一个提升的版本,但仅提升到S?。您不会得到Task<bool> operator <(Task<S> x, Task<S> y),或者IEnumerable<bool> operator < (IEnumerable<S>, IEnumerable<S>)MyFavouriteMonad<bool> operator < (MyFavouriteMonad<S>, MyFavouriteMonad<S>)。在C#中,Nullable是特殊的。 - Eric Lippert

4

如果您可以使用反射器,为什么不编译此代码:

int? value = 10;
Console.WriteLine(value == 10);

然后在反编译器中打开它?你会看到这个(确保选择“None”作为 .net 版本进行反编译):

int? value;
int? CS$0$0000;
&value = new int?(10);
CS$0$0000 = value;
Console.WriteLine((&CS$0$0000.GetValueOrDefault() != 10) ? 0 : &CS$0$0000.HasValue);

基本上,编译器会为你处理繁重的工作。当与可空类型一起使用时,它会理解 '==' 操作的含义,并相应地编译必要的检查。


因为我没有反编译器...所以我从另一个SO答案中获取了反射代码,正如我在问题开头提到的那样。 - devuxer
这展示了编译器的工作原理,但并不展示语言的工作原理。 - roim

1

Nullable<T>这个方法

public static implicit operator T?(T value)
{
  return new T?(value);
}

看起来有一个从10Nullable<int> 10的隐式转换

为了让== / !=适用于您,您可以添加

public static bool operator ==(MyNullable<T> left, MyNullable<T> right) {}
// together with:
public static implicit operator MyNullable<T>(T value) {}

应该为您提供myNullable == 10操作的支持


1
是的,但正如我在问题中所说的,对于Nullable<T>,未定义==运算符,因此如果您有两个Nullable<T>,则不应该能够执行==。 - devuxer
没错。我只是为了展示你可以这样做,所以包含了 operator == - THX-1138

1

从@zespri的回答来看,似乎是这样。+1 - devuxer

1

这是与语言相关的。在处理可空值类型上的运算符时,C# 和 Visual Basic 发出不同的代码。要理解它,您需要查看实际的 IL 代码。


你说得没错,但我认为在这种情况下,问题是编译器对于 Nullable<T> 做了一些特殊处理,而对于我的代码则没有。 - devuxer

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