为什么在参数列表评估后执行空检查?

时间:2015-09-06 19:41:44

标签: c# member invocation language-specifications

根据C#语言规范7.4.3 Function member invocation,函数成员调用的运行时处理包括以下步骤,其中M是在引用类型中声明的实例函数成员,E是实例表达式:

  1. 评估E.如果此评估导致异常,则不执行进一步的步骤。
  2. 评估参数列表。
  3. 如果E的类型是值类型,则执行装箱转换以将E转换为类型对象,并且在以下步骤中将E视为类型为对象。在这种情况下,M只能是System.Object的成员。
  4. 检查E的值是否有效。如果E的值为null,则抛出System.NullReferenceException,不执行进一步的步骤。
  5. 要调用的函数成员实现已确定... etc
  6. 我想知道为什么空检查不是第二步?如果E为空,为什么要评估参数列表?

4 个答案:

答案 0 :(得分:2)

如果要在步骤2中进行空检查,则必须为每个方法调用添加一个空检查。

就像现在一样,绝大多数方法都不需要检查实例是否为空。相反,他们尝试调用该方法,如果实例为null,则尝试获取方法表以执行此操作会导致无效的内存访问,然后由框架捕获并转换为NullReferenceException。这里运行的代码没有比在不知道实例的情况下更多的工作了。

实例必须显式检查非归零的唯一情况是优化意味着:

  1. 通过内联删除了通话。
  2. 内联调用不涉及字段访问(无论如何都会导致空引用异常)。
  3. 内联调用不涉及对同一对象的另一次调用(同上)。
  4. 实例无法显示为绝对不为空(或者有担忧)。
  5. 在这种情况下,会添加字段访问权,以与调用相同的方式触发NullReferenceException

    但是,如果规则在评估参数之前需要进行空检查,则需要为每个调用添加一个显式检查。在实践中,它意味着你在尝试导致NullReferenceException抛出的东西之前抛出了NullReferenceException。 (他们无法删除将低地址内存访问冲突转换为NullReferenceException的逻辑,因为它仍以其他方式出现。)

    因此,您建议的规则在实践中需要做更多的工作。

    相关:

    C#仅添加了针对在null实例上调用方法的规则,当它已经在.NET的开发中被内部使用时,虽然尚未公开发布。

    通常,在.NET中的空实例上调用非虚方法通过编译为CIL指令call而不是callvirt是完全合法的。 (就此而言,您可以以非实际相同的方式调用虚拟方法,这是对base的调用的工作方式)。只要实例上没有字段访问或虚拟方法调用(实际上很少见,但可能会发生),这将起作用。

    在此之前,规则是只有在方法是虚拟的情况下才需要进行空检查。

    这与以前一样;使用callvirt调用该方法,并在调用空引用时捕获内存访问冲突。

    当规则被更改为(不幸的是,IMO)禁止对null对象的任何调用时,这是通过将编译更改为使用callvirt来完成的,即使该方法不是虚拟的,因此如果发生内存访问冲突,则会发生实例为null,结果NullReferenceException随之而来。

答案 1 :(得分:1)

我认为这是一个定义问题,但我可以想到为什么这是一个方便的顺序的几个原因:

  • 本质上,对象的方法是函数表中的函数,对象引用(或指针)是另一个参数。我不确定调用约定是什么,但参数评估的规则是从左到右。如果约定是将指针放在右侧,那么最后检查它是有意义的,因为它的值已经知道。
  • 同样,如果你认为this - 引用是方法调用的(隐式)参数,它实际上是要检查的第一个参数,在任何参数检查之前你已经在体内编写了方法。这是有意义的:评估所有参数,并且在评估之后,它们都被检查,this - 首先参考。
  • 如果一个论证本身就是一个带有副作用的表达,那么在评估顺序上要非常明确是有意义的。在这种情况下,副作用将始终触发(除非先前的参数引发异常)。
  • 如果你有一个前后条件库,这些需要知道之前参数的值来调用方法。您可能想知道参数是否有效,无论它们被调用的对象是否为null
  • 在此检查之前,可能需要进行拳击*,这可能是相对昂贵的一步。你可能希望尽可能做到最新。
  • 扩展方法可以应用于null对象。此顺序确保扩展方法和实例方法的行为相同(在扩展方法中,您只能检查方法体内的null,即评估参数后)。
  • 对象不必位于当前系统上,也不必位于当前处理器的内存中。在OO客厅(想想Bertrand Meyer)中,方法调用本质上是向对象发送消息。显然,从这个角度来看,必须首先构造消息,然后才能发送消息。在经典的COM和DCOM中,这是类似的:构造消息(即,评估参数)然后发送。如果目标似乎不存在,则消失,销毁,引发错误。但是这种情况下的顺序可能不同。我不确定这是一个参数(处理进程外对象),但它可能与COM互操作性有关。

我意识到这些参数中的每一个都可以有一个反驳(除了最后一个),但总的来说,我认为它有利于尽可能晚地检查null的对象。

* p拳击通常并不昂贵,但如果你的参数列表很小(零或一)并且不需要进一步评估,那么与参数的虚拟无操作相比,装箱相对相对昂贵评估(由于hvd的评论而更新)。

答案 2 :(得分:0)

也许是因为在第3步中,E类型可以转换为null。通过在步骤2中使用过滤器,您可以允许传递的值在步骤3中可以转为null,因此需要另一个过滤器。

答案 3 :(得分:0)

事实上的参数评估实际上在一些其他编程范例中是不同的,例如在使用LISP的函数编程中,或者使用Prolog等进行逻辑编程。

但是在过程式和面向对象的编程语言中,通常在执行实际调用之前评估函数参数。我不知道它是否必须,但它在C,C ++,Java,C#,Pascal等中使用它们。它们遵循相同的原则。

但是,不要将此与短路规则适用的评估条件混合使用。