为什么NullReferenceException不包含有关null的信息?


73

NullReferenceException背后的设计决策是什么,除了基类数据(如stacktrace)外,不包含任何运行时特定的信息?Visual Studio是否有扩展程序可以立即告诉您表达式的哪一部分为空?


2
如果我没记错的话,ReSharper可以告诉您表达式是否可以引发此类异常。
Etienne de Martel

4
Reshaper可以预测某些东西可能会引发此类异常。
jcolebrand

你们都是对的。但是,即使是非常好的助手,ReSharper也有误报和误报的两种情况。我经常不得不通过评论手动禁用通知,因为我从数学上确定了此类例外的可能性。
USR-本地ΕΨΗΕΛΩΝ

@djechelon〜因此,我反对他的声明的原因。它不知道会发生,它可以只知道它可能会发生(您的评论)
jcolebrand

@drachenstern:不用担心,我赞成您的评论,我只是想用我的评论来扩大它,强调一个事实,即可能还会有假阴性(您可以预测它可能,但是您可以预测一个声明会不会'不会抛出异常?)。就是这么简单;)
usr-local-ΕΨΗΕΛΩΝ2011年

Answers:


77

NRE是非常底层的异常。当要求处理器从低于64K的地址读取数据时,这是处理器生成的硬件异常(“陷阱”)。虚拟内存空间的该区域始终未映射,专门用于捕获指针错误。它以AccessViolation开头,当地址小于0x00010000时由CLR变为NRE。那时,异常的上下文很少,所知道的只是引起陷阱的机器代码指令的地址。

无法将机器代码指令地址反向工程回程序中的命名变量。重要的是,它的工作原理是这样,抖动就必须产生非常否则低效的代码。可以合理地做的就是恢复源代码行号。这需要包含行号信息的调试信息(.pdb)。CLR知道如何读取.pdb文件,并使用它来生成异常的堆栈跟踪。但是,由于JIT优化器执行的优化,这通常仍然不准确,会四处移动代码。您只会获得与Debug构建版本匹配的保证。Release版本的PDB不包含源行号信息的原因。您可以更改它。

这个问题有一个非常简单的解决方案。自己检查null并生成自己的异常,然后再让运行时执行。该测试非常便宜,不到一纳秒。


3
在错误消息中提供全面的信息是计算机科学的一个有价值的目标。我希望有一天,无论是语言还是计算机,都被改变为“将所有内容都放在保护子句中”并不是最好的答案。
格雷格,

15
我当然希望不会。如果容易的话,我将失去一份高薪的工作。
汉斯·帕桑

3
@HansPassant .NET 4.6.2显然允许您查看null发生NRE时的表达式。知道到底怎么可能吗?blogs.msdn.microsoft.com/dotnet/2016/08/02/...
吉荣Vannevel

1
当它落在我的机器上时,我会弄清楚的。最近,我对更新没有太多的错误感到兴奋。
汉斯·帕桑

2
我要打电话给BS。我们可以获得NRE的行号,该行号不在机器代码中,而且级别太低,无法进行反向工程。此时的stacktrace生成至少应能够为您提供生成异常的语句。
杰里米·霍洛瓦奇

27

您可以将Visual Studio设置为NullReferenceException立即抛出,而不是首先在catch块中断。

  • 除错
  • 例外情况
  • 通用语言
  • 运行时例外系统
  • System.NullReferenceException(选中复选框)

然后,您将在导致的行中中断NullReferenceException


这次真是万分感谢; 您真的节省了一天!我认为这应该是选择的答案。
Krythic 2015年

如果表达式为多行并且在该行上具有50个字段,则不是很有帮助
。– Filip

5

没有简单的方法可以确定运行时为空的内容–必须从IL中反编译并推断必要的信息,该信息足够低级,它所知道的只是“堆栈顶部的项目为空” ”,但该物品如何到达那里。

(而且我不知道这样的扩展,但这将是一个不错的调试器增强)


好吧,它必须在堆栈上处于中间过程才能知道它弹出的内容为空,所以我不知道它如何不知道堆栈框架,但它可以知道堆栈顶部的内容是null ...
jcolebrand

我必须纠正你。IL语言显然对堆栈顶部的对象一无所知,但是如果您有调试符号,则可能会有更多的提示。在我的答案中,类似一个语句的复杂度足以将其拆分为许多IL语句:只需将每个调用绑定到所涉及的对象之一,就可以得到null。我不知道当前的调试符号是否可以帮助实现类似的功能,但我同意(+1)这将是一项不错而又便宜的增强功能!
USR-本地ΕΨΗΕΛΩΝ

4

NullReferenceException由运行时抛出,而不是由语言抛出。运行时不知道引用来自何处。它只知道一条指令试图使用空引用。

实际上,该异常是“可能”是本机Windows“无效访问冲突” SEH异常的结果,该异常被运行时捕获并转换为IL异常(“可能”:我没有检查是否是这种情况,但是这是我猜测JIT代码最有效的方式)。


3

并非有关代码的所有信息都在运行时可用。例如,本地变量名称不可用。因此,不可能抛出类似“本地变量myObj为空,但其成员已被调用”之类的消息的异常。此外,在运行时还有许多非用户编写的对象(例如,为闭包/匿名类型/迭代器等生成的类),它们也可能是null引用异常的来源。


但是它至少可以说出它采用的是什么方法吗?
jcolebrand

2
@drachenstern:当前方法将在异常的堆栈跟踪之上。
德克·沃尔玛

@drachenstern-通常可以从stacktrace轻松获得
Marc Gravell

您可以检查堆栈跟踪。我不明白您为什么认为NRE不包含它。
Victor Haydin

在Visual Studio中调用堆栈:Debug ... Windows ... Call Stack。
克里斯·贝伦斯

0

如果有调试符号,则可以跟踪引起异常的行。如果足够简单,那么您将直接获得null价值。否则(请考虑a.Value = b.Do(c.GetX(),d.GetY(z.ToString()));),您必须使用IDE进行调试


2
这就是为什么我避免嵌套调用的原因之一……只要您使用临时变量,它就会变得更加容易
Thomas Levesque

正是我的想法!!!但是有时您被召唤来调试别人的代码,而现在您知道为什么别人是愚蠢的;)
usr-local-ΕΨΗΕΛΩΝ2011年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.