我会直接把这个问题写给杰弗里里希特,但上次他没有回答我:)所以我会尽力在你的帮助下得到答案,伙计们:)
在第10页的第9版“CLR via C#”一书中,Jeffrey写道:
void M3() {
Employee e;
e = new Manager();
year = e.GetYearsEmployed();
...
}
M3调用中的下一行代码 员工的非虚拟实例 GetYearsEmployed方法。打电话的时候 一个非虚拟实例方法,即JIT 编译器找到那个类型对象 对应于的类型 用于拨打电话的变量。 在这种情况下,变量e是 定义为员工。 (如果 员工类型没有定义方法 被调用,JIT编译器走了 将类层次结构向下移向Object 寻找这种方法。它可以做到 这是因为每个类型对象都有一个 其中的字段指的是它的基础 类型;此信息未显示在 数字。)然后,JIT编译器 在类型对象中查找条目 方法表引用该方法 被调用,JITs方法(如果 必要的),然后调用JITted 代码。
当我第一次读到这篇文章时,我认为在JIT-ting期间沿着类层次结构寻找方法是没有效果的。在编译阶段很容易找到该方法。但我相信杰弗里。我在另一个论坛上发布了这个信息,另一个人证实了我的疑问,这是奇怪的,并且会无效,而且似乎是错误的信息。
实际上,如果你在反编译器中查找相应的IL代码,例如ILDasm或Reflector(我已经检入过),你会看到IL有一个callvirt指令从基类调用方法,所以JIT不需要查看方法在运行时所在的类:
public class EmployeeBase
{
public int GetYearsEmployed() { return 1; }
}
public class Employee : EmployeeBase
{
public void SomeOtherMethod() { }
}
public class Manager : Employee
{
public void GenProgressReport() { }
}
...
Employee e;
e = new Manager();
int years = e.GetYearsEmployed();
产生的IL是:
L_0000: nop
L_0001: newobj instance void TestProj.Form1/Manager::.ctor()
L_0006: stloc.0
L_0007: ldloc.0
L_0008: callvirt instance int32 TestProj.Form1/EmployeeBase::GetYearsEmployed()
你知道吗?编译器已经发现该方法不在Employee类中,而是在EmployeeBase类中,并且发出了正确的调用。但是从Richter的话来说,JIT必须发现该方法实际上是在运行时位于EmployeeBase类中。
杰弗里里希特错了吗?或者我不明白?
答案 0 :(得分:2)
C#编译器完全解析非虚拟方法,没有摆动空间。如果在编译调用者之后派生的非虚方法具有相同的签名,则CLR仍将调用C#编译器选择的“固定”方法。这是为了避免脆弱的基类问题。
如果您想要动态方法解析,请使用virtual
。如果您不使用virtual
,则会获得完全静态分辨率。你的选择。成为this
指针的对象引用的运行时类型在非虚拟方法的解析中无关紧要(csc.exe都不适用于CLR JIT)。
JIT将始终调用精确选择的方法。如果方法不存在,它将抛出异常(可能是因为被调用者DLL已被更改)。它不会使用不同的方法。
callvirt
也可以调用非虚方法。它用于执行空检查。它以这种方式定义,并且定义C#以对每次调用执行空检查。
答案 1 :(得分:0)
根据我的理解,并使用您的示例: 引擎盖下:
基类中的VIRTUAL方法将在派生类方法表中具有条目。这意味着'object'类型中的所有虚方法都可以在其所有派生类方法表中使用。
NON虚方法(如示例代码中所示),派生类中没有提供的功能实际上在派生类方法表中实际上没有条目!
为了检查这一点,我在WinDbg中运行代码来检查 Manager 类的方法表。
MethodDesc表条目方法De JIT名称
506a4960 503a6728 PreJIT System.Object.ToString()
50698790 503a6730 PreJIT System.Object.Equals(System.Object)
50698360 503a6750 PreJIT System.Object.GetHashCode()
506916f0 503a6764 PreJIT System.Object.Finalize()
001b00c8 00143904 JIT Manager..ctor()
0014c065 001438f8 NONE Manager.GenProgressReport()
所以,我可以看到对象的虚拟对象方法,但我看不到实际的GetYearsEmployed方法,因为它不是虚拟的,也没有派生的实现。 顺便说一句,通过相同的概念,您无法在派生类中看到SomeOtherMethod函数。
但是,您可以调用这些函数,只是它们不在方法表中。我可能是不正确的,但我相信调用堆栈是为了找到它们。也许这就是里希特先生在他的书中的意思。我发现他的书很难读,但那是因为概念很复杂而且比我聪明:)
我不确定IL会反映出这个问题。我相信它可能是IL下面的一层,这就是为什么我用Windbg来看一看。我想你可以使用windbg来看看它在堆栈中行走......
答案 2 :(得分:0)
正如@usr在类似问题中所回答的那样,我发布了How is non-virtual instance method inheritance resolved?:
运行时通常表示“代码运行的时间/时间”。 JIT 此处的分辨率仅在代码运行之前涉及一次。什么的 JIT没有被称为“在运行时”。
也用杰弗里的话来说
JIT编译器定位与其类型对应的类型对象 用于拨打电话的变量。
此处的变量类型我认为是“由元数据标记指定的类”(ECMA 335 III.3.19调用),基于JIT解析方法目的地。
C#编译器总是找出正确的调用方法,并将该信息放入元数据标记中。所以JIT永远不必“走下阶级层次”。 (但如果您手动将元数据标记更改为继承的方法,则可以使用
) class A
{
public static void Foo() {Console.WriteLine(1); }
public void Bar() { Console.WriteLine(2); }
}
class B : A {}
class C : B {}
static void Main()
{
C.Foo();
new C().Bar();
C x = new C();
x.Bar();
Console.ReadKey();
}
IL_0000: call void ConsoleApplication5.Program/A::Foo() // change to B::Foo()
IL_0005: newobj instance void ConsoleApplication5.Program/C::.ctor()
IL_000a: call instance void ConsoleApplication5.Program/A::Bar() // change to B::Bar()
IL_000f: newobj instance void ConsoleApplication5.Program/C::.ctor()
IL_0014: stloc.0
IL_0015: ldloc.0
IL_0016: callvirt instance void ConsoleApplication5.Program/A::Bar() // change to B::Bar()
IL_001b: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
IL_0020: pop
IL_0021: ret
如果我们使用Ildasm + Ilasm将A::Foo()
更改为B::Foo()
,并将A::Bar()
更改为B.Bar()
,则应用程序运行正常。