在我看来,我的大量调试时间都花在了寻找复杂语句中的空引用异常上。例如:
For Each game As IHomeGame in _GamesToOpen.GetIterator()
为什么当我得到 NullReferenceException 时,我可以得到堆栈跟踪中的行号,但不能得到等于 null 的对象的名称。换句话说,为什么:
Object reference not set to an instance of an object.
而不是
_GamesToOpen is not set to an instance of an object.
或
Anonymous object returned by _GamesToOpen.GetIterator() is null.
或
game was set to null.
这严格来说是一种设计选择,旨在保护代码的匿名性,还是编译器设计中有令人信服的理由不将此信息包含在调试时异常中?
异常是运行时的事情,变量是编译时的事情。
事实上,您示例中的变量是一个表达式。表达式并不总是简单的变量。在运行时,将计算表达式并在结果对象上调用该方法。如果该表达式的值为
null
,运行时将抛出 NullReferenceException
。假设如下:
Dim a as New MyObject
Dim b as String = MyObject.GetNullValue().ToString()
如果
GetNullValue()
方法返回 null
,运行时应返回什么错误消息?
对于像 Java 这样编译为由 VM 解释的字节码的语言,假设您有一个带有字段
X
的类 x
,并且其值是 null
以供参考。如果你写
x.foo()
字节码可能如下所示:
push Xref >> top of stack is ref to instance of X with X.x = null
getField x >> pops Xref, pushes 'null' on the stack
invokeMethod foo >> pops 'null' -> runtime exception
关键在于,需要堆栈上的非空引用才能对其进行操作的操作(例如示例中的 invokeMethod)无法也不知道该空引用来自何处。
捕获此问题以进行调试的一种简单方法是在使用之前放置 Assert 语句 一个对象,检查 null 并输出有意义的消息。
在发布版本中,变量名称从符号中删除,代码甚至可能被优化为没有变量的特定内存位置,而只是将引用保留在其中一个寄存器中(取决于变量的范围)用法)。因此,可能无法从引用位置推断出变量的名称。
在调试版本中,有更多有关变量的信息。但是,无论构建风格如何,异常对象都需要以相同的方式工作。因此,它会作用于它可以以任何方式访问的最少信息。
有几件事......
1)当你做出自己的例外时,请记住这一点(如果你为此感到恼火,如果你这样做是为了其他事情,其他人也会对你感到恼火)。 鉴于异常路径根本不应该是典型路径,因此花费时间使异常具有有用的信息是非常值得的。
2)作为一般编程实践,采用这种风格,你遇到的问题会少得多(是的,你的代码行数会更长,但你会节省很多时间):
a) 永远不要做 a.b().c(); x = a.b(); x.c(); (在单独的行上)这样您就可以看到 a 为 null 或者 a.b() 的返回是否为 null。
b) 永远不要将方法调用的返回值作为参数传递 - 始终传递变量。 a(foo());应该是 x = foo();斧头); 这个更多的是为了调试和能够看到价值。
我不知道为什么像 .net 和 Java 这样的环境不提供有关此类异常的更多信息的运行时版本,例如越界数组上的索引是什么、变量的名称当它为空时,等等...