可空类型何时会抛出异常?

30

考虑以下代码:

int? x = null;
Console.Write ("Hashcode: ");
Console.WriteLine(x.GetHashCode());
Console.Write("Type: ");
Console.WriteLine(x.GetType());
执行时,它会写入Hashcode是0,但在尝试确定x的类型时会因NullReferenceException而失败。我知道对可空类型调用的方法实际上是在基础值上调用的,因此我预计程序会在x.GetHashCode()期间失败。 那么,这两种方法之间的根本区别是什么,为什么第一个不会失败?

我能找到的唯一区别是GetHashCodevirtual... - Bart Friederichs
1
ILSpy是一个非常方便的小工具,可以帮助回答这些问题。 - Sam Axe
3
我觉得奇怪的是,从一个 Nullable<int> 中调用 GetType() 返回的是 System.Int32,而不是 System.Nullable<System.Int32> - Lasse V. Karlsen
1
值得注意的是,int? x = nullNullable<int> x = new Nullable<int>(null) 的语法糖。因此,x 是一个实际的对象,而不是一个空引用。 - Bart Friederichs
参考源 - https://github.com/Microsoft/referencesource/blob/master/mscorlib/system/nullable.cs - 没有展示GetType的处理方式,文档也没有详细说明 - https://learn.microsoft.com/en-us/dotnet/api/system.nullable-1?view=netframework-4.7.2 - Lasse V. Karlsen
4个回答

34
这是因为 int? x = null; 实际上创建了值类型 System.Nullable<int> 的实例,并将其中的 "inner" 值设置为 null(您可以通过 .HasVaue 属性进行检查)。当调用 GetHashCode 时,方法重写Nullable<int>.GetHashCode 是候选方法(因为该方法是虚拟的),现在我们有了一个Nullable<int> 实例,并执行其实例方法,非常好。
当调用GetType时,该方法是非虚拟的,因此先将Nullable<int> 实例装箱为System.Object,根据文档,装箱值为null,因此会抛出NullReferenceException

1
这一切都符合我们所看到的行为,但它在哪里有记录?你提供的页面详细说明了如何在“Nullable<T>”上运作装箱行为,但它并没有说“GetType()”需要进行装箱操作。我猜想我们需要找到关于值类型上继承方法如何工作的文档,以找到相关的部分。 - Lasse V. Karlsen
1
@LasseVågsætherKarlsen那是因为GetType是在object上定义的,而不是在struct上定义的,这会导致隐式装箱。你可以通过赋值int? x = 1然后调用GetType()来验证。结果是System.Int32,而不是Nullable<T> - Toxantron
1
@LasseVågsætherKarlsen 你可以阅读C#语言规范第4.3节“装箱和拆箱”以获取更多详细信息。 - Cheng Chen
如果我输入int? x = null;然后尝试读取它:x.Value,那么我会得到异常,而不是null "value"。如果我检查"x"本身是否为null,则答案是肯定的。 - Eru
@Eru,我在回答中使用了不正确的表达方式,感谢你指出。 - Cheng Chen
显示剩余4条评论

17
为了更清楚地阐述Danny Chen的正确答案: Nullable<T>是一个值类型。该值类型包含一个bool,表示是否为空(false表示为空),以及一个值T。
与所有其他值类型不同,可空类型不会装箱成一个箱化的Nullable<T>。它们会装箱成一个箱化的T或一个null引用。
由值类型S实现的方法被实现为具有不可见的ref S参数的方法;这就是this如何传递的。
由引用类型C实现的方法被实现为如果存在一个不可见的C参数,则如此实现;这就是this如何传递的。
有趣的情况是在一个引用基类中定义的虚拟方法,并由继承自基类的结构体重写的情况。
现在你有足够的信息来推断发生了什么。 GetHashCode是虚拟的并且被Nullable<T>覆盖,所以当你调用它时,你调用它就像有一个不可见的ref Nullable<T>参数的this一样。不发生装箱。 GetType不是虚拟的,因此无法被重写,并且在object上定义。因此,它期望一个object作为this。当在Nullable<T>上调用时,接收者必须装箱,因此可以装箱到null,因此可能会抛出异常。
如果您调用((object)x).GetHashCode(),则会看到异常。

第三和第四个关于不可见的 ref SC 的要点有些不清楚。你能详细说明一下吗? - Nisarg Shah
3
当你有class C { void M(int x) { ... } }C c = new C(); c.M(123);时,一定要让 cM 函数内变成 this。这是通过将函数调用逻辑上转换为 class C { static void M(C _this, int x) { ... }} 以及 C.M(c, 123) 来实现的。在这种情况下,this 逻辑上只是“另一个参数”。对于结构体也是如此,只不过在结构体中,this 是一个指向变量的 ref 别名。 - Eric Lippert

5
Nullable<T>.GetHashCode() 的实现如下所示:
public override int GetHashCode()
{
    if (!this.HasValue)
    {
        return 0;
    }
    return this.value.GetHashCode();
}

因此,当值为null时,它总是会返回0

x.GetType()null.GetType()相同,这将抛出对象未实例化的对象引用异常。


所以,GetHashCode 是在 Nullable 上调用的,而不是在其包含的值上调用?为什么会这样? - Кирилл Глазырин
为什么x.GetType()null.GetType()是相同的,因为实际上你有(struct).GetType()? Nullable<T>是一个值类型的结构体。 - Lasse V. Karlsen
你能否也发布GetType的实现,以防它可以为答案增加更多细节? - Souvik Ghosh

1
似乎GetHashCode进行了空值检查。(使用JetBrains查看定义)
public override int GetHashCode()
{
  if (!this.hasValue)
    return 0;
  return this.value.GetHashCode();
}

只有在确定可以调用 GetHashCode() 时,GetHashCode() 的实现才会发挥作用。 - user743382

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