对象引用未设置为对象的实例。为什么.NET不显示哪个对象为空?

105

关于这个.NET未处理异常信息:

对象引用未设置到对象的实例。

.NET为什么不显示哪个对象是null呢?

我知道我可以检查null并解决错误。然而,为什么.NET不帮忙指出哪个对象具有空引用,哪个表达式触发了NullReferenceException


2
当出现这种情况时,重写发生错误的那一行代码,先检查每个可能的结果是否为空 - 这样你就会知道具体是哪里出了问题。或者,使用 Visual Studio 强大的调试器,它会在异常发生时立即中断程序并让你看到哪里为空 :) - Patashu
5
不完全是这样,他只是询问为什么.NET框架无法帮助程序员显示哪个对象为空。我猜测这是因为性能损耗(需要使用反射),但我也不确定。 - bas
1
@bas:虽然这是正确的,但问题有点误导性,因为它应该询问“表达式的一部分”,而不是“对象”。这也解释了为什么仅仅使用反射是无法帮助的,需要一些详尽的调试信息。 - O. R. Mapper
4
我很想知道答案。这有些类似于 .net 异常没有帮助指出字典中缺少哪个键。此外,我不明白问题的支持者们在想什么。 - bas
13
术语请:一个对象从来不会为空。但是一个“对象引用”可能为空。但是,“对象引用”只是内存中的位置 - 除非您已经连接了调试器,否则它对您有什么帮助呢? - Oskar Berggren
显示剩余13条评论
5个回答

172

(有关Visual Studio 2017中新的异常助手的信息,请参见本答案末尾)


考虑以下代码:

String s = null;
Console.WriteLine(s.Length);

在第二行代码中抛出了NullReferenceException异常,你想知道为什么.NET没有告诉你是s为null导致了异常。

要理解为什么你没有得到这个信息,你应该记住执行的不是C#源代码而是IL代码:

IL_0001:  ldnull      
IL_0002:  stloc.0     // s
IL_0003:  ldloc.0     // s
IL_0004:  callvirt    System.String.get_Length
IL_0009:  call        System.Console.WriteLine

正是callvirt操作码引发了NullReferenceException异常,并且当评估堆栈上的第一个参数是null引用时(使用ldloc.0加载的参数)时会抛出该异常。

如果.NET能够确定是s为null引用,那么它应该以某种方式跟踪该参数在评估堆栈上的位置。在这种情况下,我们很容易看出是s为null引用,但是如果该值是从另一个函数调用的返回值并且未存储在任何变量中,则怎么办呢?无论如何,在像.NET虚拟机这样的虚拟机中,这种信息都不是您想跟踪的信息。


为了避免这个问题,我建议您在所有公共方法调用中执行参数空值检查(除非您允许null引用):

public void Foo(String s) {
  if (s == null)
    throw new ArgumentNullException("s");
  Console.WriteLine(s.Length);
}

如果将 null 传递给该方法,您将获得一个异常,它准确地描述了问题所在(即 s 为 null)。


四年后,Visual Studio 2017 现在有了一个新的异常助手,尝试告诉您当抛出 NullReferenceException 时哪些是 null。即使是当方法的返回值为 null 时,它也能够提供所需的信息:

Visual Studio 2017 exception helper

请注意,这仅适用于 DEBUG 构建。


5
IL代码本身并没有存储行号和源文件名称,是吗?然而,它们可以用于调试。 - O. R. Mapper
4
@MartinLiversage:确切地说,问题归结为:为什么符号文件中存储的信息不足以告诉我们代码中评估为“null”的表达式是什么。 - O. R. Mapper
2
@MartinLiversage:符号文件可以以一种可访问的方式进行访问,因此在异常发生时,除了异常消息之外,源文件和行号也可以显示在调试器的输出中。因此,问题是,为什么不包括有关返回“null”的确切信息-请注意,OP并没有声称他或她想要知道发布版本的信息,因为调试版本可能已经足够。 - O. R. Mapper
3
嗯,我们不应该忘记的是,实际上执行的不是IL本身,而是在运行时从它构建出来的本地代码。 - Oskar Berggren
2
@MartinLiversage:实际上,据我所知,NullReferenceException的实现确实依赖于硬件故障的触发。有关更多详细信息,请参见此问题:为什么.NET将最低地址空间(非null)中的内存访问报告为NullReferenceException? - Daniel Pryden
显示剩余15条评论

9
在以下情况下,您希望错误消息是什么样子?
(注:该文本已翻译,仅供参考)
AnyObject.GetANullObject().ToString();

private object GetANullObject()
{
  return null;
}

这里没有要报告的变量名!


2
我怀疑 OP 正在寻找源代码中返回 null 的表达式,而不是对象本身。我已经在问题下添加了相应的评论,并希望他或她能够澄清事情。如果我的怀疑是正确的,那么 OP 会期望类似于 Object reference obtained from AnyObject.GetANullObject() not set to an instance of an object. 这样的错误消息。 - O. R. Mapper
1
@O.R.Mapper 我同意。如果我有足够的声誉点来添加评论,我会将我的“答案”放在OP的评论中! - romar
1
尝试调用 XYZ 类的 ToString() 方法时出现了空引用。提供更多信息会更有帮助。 - Michael Levy
最有帮助的是一个堆栈跟踪,显示每个调用级别中导致错误的确切文件中的哪一行。哦,等等...这就是它现在所做的! - Jim Balter

1

我不确定,但可能是因为 .Net 不知道它是预定义类还是用户定义的类。如果它是预定义的,则可以为空(例如占用2个字节的字符串),但如果它是用户定义的,则必须创建一个实例,以便它知道此对象将占用多少内存。因此,在运行时会抛出错误。


1

好的,这取决于微软的工程师来回答。但是你可以使用调试器并添加观察点来找出其中哪个有问题。

然而,异常是NullReferenceException,这意味着引用不存在。你无法获取根本未被创建的对象。

但为什么.NET不告诉我们哪个对象为空?因为它不知道哪个对象为空。该对象根本不存在!

当我说C#编译成.NET IL代码时也是如此。.NET IL代码不知道名称或表达式,它只知道引用及其位置。同样,在这里,你无法获取不存在的内容。表达式或变量名不存在。

哲学:如果一开始没有蛋,就不能制作煎蛋卷。


3
那也不是一个答案 :) - bas
4
“那就由微软的工程师来回答这个问题吧。” 让他们来阐明这个问题,而不是陈述显而易见的事实。 - bas
@bas 我们至少可以决定什么是合乎逻辑的。从逻辑上讲,该对象不存在。你如何通过异常捕获它,然后打印出对象的名称?它根本不存在。甚至不在堆栈上。 - Aniket Inge
@Aniket:我怀疑 OP 寻找的是源代码中返回“null”的表达式,而不是对象本身。我已经在问题下添加了相应的评论,并希望他或她能澄清事情。 - O. R. Mapper
@O.R.Mapper,他计划从哪里获取表达式? - Aniket Inge
显示剩余4条评论

-2

很好的问题。消息框几乎没什么用。即使它被埋藏在参考定义的深处,一些类或程序集或文件或其他信息都比他们目前提供的要好(可以说是好过没有)。

您最好的选择是在带有调试信息的调试器中运行它,您的IDE会在有问题的行上断点(清楚地表明实际上是有可用的有用信息的)。


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