根据 C# 语言规范 7.4.3 Function member invocation函数成员调用的运行时处理包括以下步骤,其中M是在引用类型中声明的实例函数成员,E是实例表达式:
- E 被评估。如果此评估导致异常,则不会执行进一步的步骤。
- 评估参数列表。
- 如果E的类型为值类型,则进行装箱转换,将E转换为object类型,在后续步骤中将E视为object类型。在这种情况下,M 只能是 System.Object 的成员。
- 检查 E 的值是否有效。如果 E 的值为 null,则抛出 System.NullReferenceException 并且不执行进一步的步骤。
- 要调用的函数成员实现已确定...等等
我想知道为什么空检查不是第二步?如果 E 为空,为什么要评估参数列表?
最佳答案
如果您要在第 2 步进行 null 检查,则必须为每个方法调用添加 null 检查。
就像现在一样,绝大多数方法不需要检查实例是否为空。相反,他们尝试调用该方法,如果实例为空,则尝试获取方法表来执行此操作会导致无效的内存访问,然后被捕获并变成 NullReferenceException
框架。与先验已知实例不为空的情况相比,这里的运行代码没有更多的工作。
只有当优化意味着:
- 通过内联删除了调用。
- 内联调用不涉及字段访问(无论如何都会导致空引用异常)。
- 内联调用不涉及对同一对象的另一个调用(同上)。
- 无法证明该实例绝对不为空(否则会有担忧)。
在这种情况下,添加了一个字段访问以触发 NullReferenceException
,就像调用一样。
但是,如果规则要求在评估参数之前进行空检查,则需要为每个调用添加显式检查。在实践中,这意味着您在尝试可能导致抛出 NullReferenceException
的操作之前抛出了一个 NullReferenceException
。 (他们无法删除将低地址内存访问冲突变成 NullReferenceException
的逻辑,因为它仍然以其他方式出现)。
因此,您建议的规则在实践中需要做更多的工作。
相关:
C# 仅在它已在 .NET 开发的内部使用时添加了禁止在 null 实例上调用方法的规则,尽管尚未公开发布。
毕竟,通过编译为 CIL 指令 call
而不是 callvirt
,在 .NET 中调用空实例上的非虚方法通常是完全合法的。 (就此而言,您可以以相同的方式非虚拟地调用虚拟方法,这就是调用 base
的方式)。只要实例上没有字段访问或虚方法调用(这在实践中很少见,但可能会发生),这就会起作用。
在此之前,规则是只有当方法是虚方法时才需要进行 null 检查。
这与之前的方法相同;使用 callvirt
调用该方法,如果在空引用上调用该方法,则捕获内存访问冲突。
当规则更改为(不幸的是,IMO)禁止对空对象的任何调用时,这是通过将编译更改为使用 callvirt
来完成的,即使该方法不是虚拟的,因此内存访问如果实例为 null,则会发生违规,并且会产生 NullReferenceException
。
关于c# - 为什么在参数列表评估后执行空检查?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32427549/