为什么“对象引用未设置为对象的实例”不能告诉我们哪个对象?


39

我们正在启动一个系统,有时我们会收到NullReferenceException消息中著名的异常Object reference not set to an instance of an object

但是,在一种有将近20个对象的方法中,记录一个对象为空的日志实际上根本没有用。这就像告诉您,当您是研讨会的安全人员时,100位参加者中的一个人是恐怖分子。真的对您毫无用处。如果要检测哪个人是威胁性的人,则应该获取更多信息。

同样,如果要删除该错误,则需要知道哪个对象为空。

现在,有些事情困扰了我几个月,这就是:

.NET为什么不给我们名称或对象引用的类型(至少为null)?。它不能从反射或任何其他来源理解类型吗?

另外,了解哪个对象为空的最佳实践是什么?我们是否应该始终在这些上下文中手动测试对象的可空性并记录结果?有没有更好的办法?

更新: 异常The system cannot find the file specified具有相同的性质。在附加到进程并进行调试之前,您无法找到哪个文件。我猜这些类型的异常会变得更聪明。如果.NET可以告诉我们c:\temp.txt doesn't exist.而不是一般性消息,会更好吗?作为开发人员,我投赞成票。


14
异常应包括带有行号的堆栈跟踪。我将从那里开始调查那条线上访问的每个对象。
PersonalNexus

2
另外,我一直想知道为什么Visual Studio中的异常帮助器对话框包含new用于创建类实例的“有用”提示。这样的提示什么时候真正有用?
PersonalNexus

1
如果您有十个对象调用链,那么设计中的耦合就会出现问题。
皮特·柯坎


9
我喜欢这个问题的每个答案都遵循“使用调试器,记录您的错误,检查是否为空,无论如何都是您的错”的方式,这些答案都不能回答问题,而应归咎于您。只有在stackoverflow上,才有人真正给您答案(我认为这对于VM来说太费劲了)。但是,实际上,唯一能够正确回答此问题的人是Microsoft从事框架工作的人员。
罗克兰

Answers:


28

NullReferenceException基本上告诉你:你错了这样做。仅此而已。相反,它不是完善的调试工具。在这种情况下,我会说你做错了,因为

  • 有一个NullReferenceException
  • 您并没有以了解原因/发生地点的方式来阻止它
  • 也可能是:需要20个对象的方法似乎有点过时

我非常乐于在出现问题之前检查所有内容,并为开发人员提供良好的信息。简而言之:使用ArgumentNullException和喜欢的东西写支票,然后自己写名字。这是一个示例:

void Method(string a, SomeObject b)
{
    if (a == null) throw ArgumentNullException("a");
    if (b == null) throw ArgumentNullException("b");

    // See how nice this is, and what peace of mind this provides? As long as
    // nothing modifies a or b you can use them here and be 100% sure they're not
    // null. Should they be when entering the method, at least you know which one
    // is null.
    var c = FetchSomeObject();
    if(c == null)
    {
        throw InvalidOperationException("Fetching B failed!!");
    }

    // etc.
}

您也可以查看Code Contracts,它有一些古怪之处,但是效果很好,可以节省一些键入时间。


17
@SaeedNeamati您并不是在暗示,因为您的代码库很大,所以您不应该进行体面的错误检查,对吗?Imo项目越大,角色或错误检查和报告就越重要。
stijn 2012年

6
即使没有回答实际问题(可能只有Anders Hejlsberg可以回答),但+1都是很好的建议。
罗斯·帕特森

12
+1是极好的建议。@SaeedNeamati,您应该听听此建议。您的问题是由于粗心和代码缺乏专业素养所致。如果您没有时间编写好的代码,那么您会遇到更大的问题……
MattDavey 2012年

3
如果您没有时间编写良好的代码,那么您肯定没有时间编写不良的代码。编写错误的代码比编写好的代码要花费更长的时间。除非您真的不在乎错误。而且,如果您真的不关心错误,为什么还要编写程序呢?
MarkJ 2012年

5
@SaeedNeamati I only say that checking every object to get sure that it's not null, is not a good method认真。这是最好的方法。不仅要检查是否为null,还要检查每个参数的合理值。您越早发现错误,就越容易找到原因。您不需要回溯堆栈跟踪中的多个级别来查找引起调用。
jgauffin 2012年

19

它确实应该准确显示您要调用的内容。这就像在说:“有一个问题。您需要解决它。我知道这是什么。我不会告诉您。您要解决这个问题。”具有讽刺意味的是,这个堆栈溢出有一半的答案。

所以,举例来说,如果您得到了这个,它将有多大用处...

Object reference (HttpContext.Current) not set to instance of an object

...?必须进入代码,逐步执行,然后确定您要调用的东西null很好,但是为什么不给我们一点帮助呢?

我同意单步执行代码来获得答案通常是有用的(因为您可能会发现更多的信息),但是如果NullReferenceException文本更像上面的示例,通常会节省很多时间和沮丧。

只是在说。


运行时应该如何知道什么是null?
2013年

3
运行时提供的信息多于提供的信息。无论它有什么有用的信息,都应该提供。我就是这么说 在回答您的问题时,为什么不知道呢?如果您知道答案,则可能会提供比您更好的评论。
LiverpoolsNumber9'9

同意,我也会说同样的话KeyNotFoundException以及其他许多烦恼...
sinelaw 2013年

实际上,在使用null取消引用的情况下,这似乎是CLR取消引用的方式的一种限制(感谢Shahrooz Jefri对OP的问题的评论)
sinelaw


4

您的日志应包括堆栈跟踪-通常会提示您方法中哪一行存在问题。您可能需要使发布版本中包含PDB符号,以便您了解错误所在的行。

当然,在这种情况下它不会为您提供帮助:

Foo.Bar.Baz.DoSomething()

告诉不问原则可以帮助避免这样的代码。

至于为什么不包括这些信息,我不确定-我怀疑至少在调试版本中,如果他们真的愿意,他们会解决。进行故障转储并在WinDBG中打开可能会有所帮助。


2

异常已被创建为在呼叫链上发出异常非致命状况信号的工具。也就是说,它们并非设计为调试工具。

如果空指针异常是调试工具,它将立即终止程序执行,从而允许调试器进行连接,并将其指向指示行。这将为程序员提供所有可用的上下文信息。(尽管有点粗略,但由于使用空指针访问,Segfault几乎可以在C中执行此操作。)

但是,空指针异常被设计为有效的运行时条件,可以在正常程序流中引发和捕获该异常。因此,需要考虑性能方面的考虑。而且,对异常消息的任何自定义都要求在运行时创建,连接和销毁字符串对象。这样,静态消息无疑会更快。

我并不是说,运行时不能以一种会产生间接引用名称的方式进行编程。可以做到的。这只会使异常比它们慢。如果有人足够关心的话,甚至可以将这种功能切换为可切换功能,这样就不会减慢生产代码的速度,但是可以简化调试过程。但是由于某种原因,似乎没有人足够在意。


1

我认为Cromulent钉在了头上,但是很明显的一点是,如果得到的NullReferenceException是未初始化的变量。您将大约20个对象传递给方法的论点不能说是缓解措施:作为代码块的创建者,您必须负责代码的工作,其中包括对代码库其余部分的遵从性,例如以及正确,正确地利用变量等。

它繁琐,乏味,有时甚至很乏味,但最后的好处是值得的:很多时候,我不得不浏览重达几GB的日志文件,并且它们几乎总是有用的。但是,在您进入该阶段之前,调试器可以为您提供帮助,并且在该阶段之前,良好的计划可以减轻很多麻烦(并且我也不意味着对您的代码解决方案采用完全工程化的方法:简单的草图和一些说明可以并且将会总比没有好)。

关于Object reference not set to an instance of an object代码,代码无法猜测我们可能喜欢的值:这是我们作为程序员的工作,并且仅表示您已传入未初始化的变量。


您知道人们曾经使用汇编语言提供像这样的理由吗?而不使用自动垃圾收集?并能够进行算术运算(以十六进制表示)?“繁琐,乏味,有时乏味”是我们描述应该自动化的工作的方式。
Spike0xff

0

学习使用调试器。这正是它设计的目的。在有问题的方法上设置一个断点,然后离开。

只需单步执行代码,然后确切地查看所有变量在特定点处的值。

编辑:坦白地说,我很震惊,没有其他人提到使用调试器。


2
因此,您是说使用调试器可以轻松重现所有异常吗?
jgauffin

@jgauffin我的意思是,您可以使用调试器来查看为什么在现实世界代码中引发异常,而不是合成单元测试,而合成单元测试可能并未真正完全测试所讨论的代码,或者单元测试本身可能存在导致问题的错误他们错过了真实代码中的错误。调试器几乎胜过我能想到的任何其他工具(也许像Valgrind或DTrace之类的东西除外)。
Cromulent

1
所以您是说我们总是可以访问发生故障的计算机,并且调试比单元测试好吗?
jgauffin

@jgauffin我是说,如果您有选择的话,那么优先使用调试器而不是其他工具。当然,如果您没有选择的余地,那将是一个不起眼的初学者。显然,这个问题并非如此,这就是为什么我给出我的答案。如果问题是如何在没有远程调试(或本地调试)方法的客户端计算机上解决此问题,我的答案将是不同的。您似乎正在试图以与当前问题无关的方式来扭曲我的答案。
Cromulent

0

如果您想使用@stijn的答案并将空检查放入代码中,则此代码段应会有所帮助。 以下是有关代码段的一些信息。完成设置后,只需键入argnull,两次单击选项卡,然后填写空白。

<CodeSnippet Format="1.0.0">
  <Header>
    <Title>EnsureArgNotNull</Title>
    <Shortcut>argnull</Shortcut>
  </Header>
  <Snippet>
    <Declarations>
      <Literal>
        <ID>argument</ID>
        <ToolTip>The name of the argument that shouldn't be null</ToolTip>
        <Default>arg</Default>
      </Literal>
    </Declarations>
    <Code Language="CSharp">
      <![CDATA[if ($argument$ == null) throw new ArgumentNullException("$argument$");$end$]]>
    </Code>
  </Snippet>
</CodeSnippet>

注意:c#6现在具有nameof您的代码段throw new ArgumentNullException(nameof($argument$)),其优点是不包括魔术常数,由编译器检查以及使用重构工具更好地工作
stijn 2015年
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.