为什么C#允许你“抛出null”?

94

在编写一些特别复杂的异常处理代码时,有人问,你不需要确保你的异常对象不为空吗?我回答说,当然不需要,但后来决定尝试一下。显然,你可以抛出 null,但它仍然被转换为异常。

为什么允许这样做呢?

throw null;

在这个代码片段中,幸运的是'ex'不是null,但它是否可能为null呢?
try
{
  throw null;
}
catch (Exception ex)
{
  //can ex ever be null?

  //thankfully, it isn't null, but is
  //ex is System.NullReferenceException
}

11
你确定你没有看到一个.NET异常(空引用)覆盖了你自己的抛出吗? - micahtan
4
你会认为编译器至少会发出一个警告,说明试图直接“throw null;” - Andy White
不,我不确定。框架很可能正在尝试对我的对象进行某些操作,当它为空时,框架会抛出“空引用异常”..但最终,我想确保“ex”永远不会为空。 - quip
1
@Andy:在编程语言的其他情况下,警告可能是有意义的,但对于throw这个本来就是为了抛出异常而存在的语句来说,警告并没有太多价值。 - Mehrdad Afshari
@Mehrdad - 是的,但我怀疑任何开发人员都不会故意“抛出null”,并希望看到NullReferenceException。也许它应该是一个完整的构建错误,就像在if语句中进行不正确的=比较一样。 - Andy White
有趣的是Java也有相同的行为。请查看https://dev59.com/LmMm5IYBdhLWcg3wfvA2和https://dev59.com/Dmw15IYBdhLWcg3wd7pj。 - Mel
7个回答

105

因为语言规范在那里期望一个类型为 System.Exception 的表达式(因此,在该上下文中,null 是有效的),并且不限制该表达式必须非空。一般来说,它无法检测该表达式的值是否为 null。它将不得不解决停机问题。运行时仍然需要处理 null 情况。请参见:

Exception ex = null;
if (conditionThatDependsOnSomeInput) 
    ex = new Exception();
throw ex; 
他们当然可以使抛出 null 字面量的特定情况无效,但这并没有什么帮助,为什么浪费规范空间并减少一致性来换取少量的利益呢?
免责声明(在 Eric Lippert 拍我之前):这是我自己关于这个设计决策背后推理的猜测。当然,我没有参加过这个设计会议 ;)
你第二个问题的答案是,一个在 catch 子句中捕获的表达式变量是否可能为空值:虽然 C# 规范对其他语言是否会引起 null 异常传播保持沉默,但它确实定义了异常传播的方式:
如果有 catch 子句,则按照出现的顺序依次检查以找到适合的异常处理程序。第一个指定异常类型或异常类型基类的 catch 子句被视为匹配项。对于任何异常类型,通用 catch 子句都被视为匹配项。...
对于 null,粗体陈述是错误的。因此,尽管根据 C# 规范纯粹地说,我们不能说底层运行时不会抛出 null,但我们可以确定即使是这种情况,也只能由通用的 catch {} 子句处理。
对于 CLI 上的 C# 实现,我们可以参考 ECMA 335 规范。该文档定义了 CLI 内部抛出的所有异常(其中没有一个是 null),并提到用户定义的异常对象由 throw 指令抛出。该指令的描述与 C# 的 throw 语句几乎完全相同(除了它不限制对象的类型为 System.Exception):
throw 指令在堆栈上抛出异常对象(类型为 O),并清空堆栈。有关异常机制的详细信息,请参见 Partition I。
[注意:虽然 CLI 允许抛出任何对象,但 CLS 描述了应用于语言互操作性的特定异常类。结束说明]
如果 obj 为 null,则引发 System.NullReferenceException。
正确定 CIL 确保对象始终为 null 或对象引用(即类型为 O)。
我认为这些足以得出结论,捕获的异常永远不会是 null。

我认为它最好的做法就是禁止明确的短语,例如 throw null; - FrustratedWithFormsDesigner
8
在CLR(公共语言运行时)中,实际上完全可以抛出一个不继承自System.Exception的对象。据我所知,在C#中不能这样做,但可以通过IL、C++/CLR等方式实现。有关更多信息,请参见http://msdn.microsoft.com/en-us/library/ms404228.aspx。 - technophile
@technophile:是的,我在谈论语言。在C#中,不可能抛出其他类型的异常,但可以使用通用的catch { }子句捕获它。 - Mehrdad Afshari
1
虽然编译器拒绝接受参数可能为空值的 throw 是没有意义的,但这并不意味着 throw null 就一定是合法的。编译器可以要求 throw 参数具有可辨别的类类型。像 (System.InvalidOperationException)null 这样的表达式应该在编译时是有效的(执行它应该会导致 NullReferenceException),但这并不意味着未经分类的 null 是可以接受的。 - supercat
如果我查找 Window 类(WPF)的元数据,它有一个名为 ShowDialog() 的方法,其中包含 'throw null;'。 - Paul McCarthy
显示剩余4条评论

32

显然,你可以抛出 null,但它最终还是会在某个地方被转换成异常。

试图抛出一个 null 对象会导致(完全无关的)Null 引用异常。

问为什么允许抛出 null 就像问为什么允许做这个:

object o = null;
o.ToString();

8
虽然在C#中无法使用 throw 抛出 null 异常,因为 throw 会检测到并将其转换为 NullReferenceException 异常,但是可以接收 null 值...我现在正好接收到了 null 值,这导致我的 catch (本来不希望 'ex' 为空) 经历了空引用异常,然后导致我的应用程序崩溃(因为那是最后一个 catch)。
所以,虽然我们不能从 C# 中抛出 null 值,但是 netherworld 可以抛出 null 值,因此你的最外层 catch(Exception ex) 最好准备好接收它。仅供参考。

3
有趣。你能发布一些代码或者暗示一下你深层次的内容是如何实现的吗? - Peter Lillevold
我不能告诉你这是否是CLR的错误...很难追踪.NET内部发生了什么导致出现null,也不知道为什么没有调用堆栈或其他线索。我只知道我的最外层catch现在会在使用Exception参数之前检查其是否为null。 - Brian Kennedy
3
我怀疑这是CLR的一个bug或者由于不良的无法验证的代码引起的。正确的CIL只会抛出非空对象或null(后者会变成NullReferenceException)。 - Demi
我也在使用一个服务,它返回了一个空异常。你有没有弄清楚这个空异常是如何被抛出的? - themiDdlest

5

以下内容来自这里:

如果您在C#代码中使用此表达式,则会引发NullReferenceException异常。这是因为throw语句需要一个类型为Exception的对象作为其单个参数。但是,在我的示例中,这个对象非常为空。


2

我认为当你试图抛出null时,它是无法抛出的,所以在错误情况下它会抛出一个空引用异常。所以实际上你并没有抛出null,而是未能抛出null,这导致了一个异常的抛出。


2

尝试回答“..感谢' ex '不是null,但它可能吗?”:

由于我们可以说不能抛出为null的异常,因此catch子句也永远不必捕获为null的异常。因此,ex永远不可能为null。

我现在看到,实际上已经有人问过这个问题了(链接)


@Brian Kennedy在他上面的评论中告诉我们,我们可以捕获null。 - ProfK
2
@Tvde1 - 我认为这里的区别在于,是的,你可以执行throw null。但这不会_抛出一个空异常_。相反,因为throw null试图在空引用上调用方法,这随后迫使运行时抛出一个非空的NullReferenceException实例。 - Peter Lillevold

1

请记住,异常包括抛出异常的位置细节。由于构造函数不知道它将在何处抛出,因此仅有意义的是throw方法在抛出点将这些细节注入对象中。换句话说,CLR试图将数据注入null,从而触发NullReferenceException。

不确定是否确切地发生了这种情况,但它解释了这种现象。

假设这是真的(我想不到比throw null;更好的方法使ex为null),那么ex不可能为null。


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