为什么 C# 编译器生成方法调用来调用 IL 中的 BaseClass 方法
本文关键字:调用 方法 IL 中的 BaseClass 为什么 编译器 | 更新日期: 2023-09-27 18:31:30
假设我们在 C# 中有以下示例代码:
class BaseClass
{
public virtual void HelloWorld()
{
Console.WriteLine("Hello Tarik");
}
}
class DerivedClass : BaseClass
{
public override void HelloWorld()
{
base.HelloWorld();
}
}
class Program
{
static void Main(string[] args)
{
DerivedClass derived = new DerivedClass();
derived.HelloWorld();
}
}
当我输入以下代码时:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 15 (0xf)
.maxstack 1
.locals init ([0] class EnumReflection.DerivedClass derived)
IL_0000: nop
IL_0001: newobj instance void EnumReflection.DerivedClass::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: callvirt instance void EnumReflection.BaseClass::HelloWorld()
IL_000d: nop
IL_000e: ret
} // end of method Program::Main
但是,csc.exe转换derived.HelloWorld();
--> callvirt instance void EnumReflection.BaseClass::HelloWorld()
。为什么?我没有在Main
方法的任何地方提到BaseClass。
而且,如果它调用BaseClass::HelloWorld()
那么我希望call
而不是callvirt
因为它看起来直接调用BaseClass::HelloWorld()
方法。
调用将转到 BaseClass::HelloWorld,因为 BaseClass 是定义方法的类。 虚拟调度在 C# 中的工作方式是在基类上调用该方法,虚拟调度系统负责确保调用方法的最派生重写。
埃里克·利珀特(Eric Lippert)的这个答案非常有用:https://stackoverflow.com/a/5308369/385844
正如他关于该主题的博客系列一样:http://blogs.msdn.com/b/ericlippert/archive/tags/virtual+dispatch/
你知道为什么这样实现吗?如果它直接调用派生类 ToString 方法会发生什么?乍一看,这种方式对我来说并没有多大意义......
之所以以这种方式实现,是因为编译器不跟踪对象的运行时类型,只跟踪其引用的编译时类型。 使用您发布的代码,很容易看出调用将转到该方法的 DerivedClass 实现。 但是假设derived
变量是这样初始化的:
Derived derived = GetDerived();
GetDerived()
可能会返回 StillMoreDerived
的实例。 如果StillMoreDerived
(或继承链中 Derived
和 StillMoreDerived
之间的任何类)重写该方法,则调用该方法的Derived
实现是不正确的。
通过静态分析找到变量可以保存的所有可能值就是解决停止问题。 对于 .NET 程序集,问题甚至更糟,因为程序集可能不是完整的程序。 因此,编译器可以合理地证明derived
不持有对更多派生对象(或 null 引用)的引用的情况数量会很少。
添加此逻辑以便它可以发出call
而不是callvirt
指令需要多少费用? 毫无疑问,成本将远远高于由此产生的小收益。
考虑这个问题的方法是,虚拟方法定义一个可以在运行时放入方法的"槽"。当我们发出 callvirt 指令时,我们说的是"在运行时,查看此插槽中的内容并调用它"。
槽由有关声明虚拟方法的类型的方法信息标识,而不是由重写它
的类型标识。向派生方法发出 callvirt 是完全合法的;运行时将意识到派生方法与基方法的插槽相同,结果将完全相同。但从来没有任何理由这样做。如果我们通过识别声明该插槽的类型来识别插槽,则会更清楚。
请注意,即使您将DerivedClass
声明为 sealed
,也会发生这种情况。
C# 使用 callvirt
运算符调用任何实例方法(virtual
或 not),以自动获取对象引用的 null 检查 - 在调用方法时引发NullReferenceException
。否则,只有在第一次实际使用方法中类的任何实例成员时才会引发NullReferenceException
,这可能会令人惊讶。如果未使用实例成员,则该方法实际上可以成功完成,而不会引发异常。
您还应该记住,IL 不是直接执行的。它首先由 JIT 编译器编译为本机指令 - 根据是否调试进程执行许多优化。我发现 CLR 2.0 的 x86 JIT 内联了一个非虚拟方法,但称为虚拟方法 - 它也内联了Console.WriteLine
!