3

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

  1. E 被评估。如果此评估导致异常,则不执行进一步的步骤。
  2. 评估参数列表。
  3. 如果E的类型是值类型,则进行装箱转换,将E转换为object类型,在后面的步骤中E被认为是object类型。在这种情况下,M 只能是 System.Object 的成员。
  4. 检查 E 的值是否有效。如果 E 的值为 null,则抛出 System.NullReferenceException 并且不执行进一步的步骤。
  5. 确定要调用的函数成员实现...等

我想知道为什么空检查不是第二步?如果 E 为空,为什么要评估参数列表?

4

4 回答 4

2

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

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

只有当优化意味着:

  1. 该调用已通过内联删除。
  2. 内联调用不涉及字段访问(无论如何都会导致空引用异常)。
  3. 内联调用不涉及对同一对象的另一个调用(同上)。
  4. 该实例不能显示为绝对不为空(或者不用担心)。

在这种情况下,添加了一个字段访问以以NullReferenceException与调用相同的方式触发。

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

所以你建议的规则在实践中需要更多的工作。

有关的:

C# 仅在 .NET 开发中已经在内部使用时才添加了禁止在空实例上调用方法的规则,尽管尚未公开发布。

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

在此之前,规则是只有当方法是虚拟方法时才需要进行空检查。

这与以前的方法相同;callvirt如果在空引用上调用该方法并捕获内存访问冲突。

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

于 2015-09-06T21:37:46.557 回答
1

我认为总的来说,这是一个定义问题,尽管我可以想到一些为什么这是一个方便的顺序的原因:

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

我意识到这些论点中的每一个都可能有一个相反的论点(也许最后一个除外),但总的来说,我认为它有利于null尽可能晚地检查对象。

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

于 2015-09-06T20:37:53.020 回答
0

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

于 2015-09-06T19:52:43.140 回答
0

函数的参数求值实际上在其他一些编程范式中是不同的,比如用 LISP 进行函数式编程,或者用 Prolog 进行逻辑编程等。

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

但是,不要将其与适用短路规则的评估条件混为一谈。

于 2015-09-06T20:00:16.500 回答