关于.net:在描述非虚方法调用的内部时,Richter是错误的吗?

Is Richter mistaken when describing the internals of a non-virtual method call?

我会直接把这个问题写给杰弗里·里克特,但上次他没有回答我:)所以我会在你们的帮助下试着得到答案,伙计们:)

杰弗里在第108页第3版《通过C_的CLR》一书中写道:

1
2
3
4
5
6
void M3() {
  Employee e;
  e = new Manager();
  year = e.GetYearsEmployed();
  ...
}

The next line of code in M3 calls
Employee’s nonvirtual instance
GetYearsEmployed method. When calling
a nonvirtual instance method, the JIT
compiler locates the type object that
corresponds to the type of the
variable being used to make the call.
In this case, the variable e is
defined as an Employee. (If the
Employee type didn’t define the method
being called, the JIT compiler walks
down the class hierarchy toward Object
looking for this method. It can do
this because each type object has a
field in it that refers to its base
type; this information is not shown in
the figures.) Then, the JIT compiler
locates the entry in the type object’s
method table that refers to the method
being called, JITs the method (if
necessary), and then calls the JITted
code.

当我第一次读到这篇文章时,我认为沿着类层次结构在Jit-Ting期间寻找方法是不有效的。很容易找到已经在编译阶段的方法。但我相信杰弗里。我在另一个论坛上发布了这个信息,另一个家伙证实了我的怀疑,它是奇怪的,会无效的,而且它似乎是错误的信息。

实际上,如果您在解压程序中查找相应的IL代码,例如ildasm或reflector(我已签入这两个代码),您将看到IL有一个callvirt指令从基类调用该方法,因此JIT不需要查看该方法在运行时位于哪个类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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为:

1
2
3
4
5
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类中。

杰弗里·里克特搞错了吗?或者我不明白?


C编译器精确地解决了非虚拟方法,并且没有任何回旋空间。如果在编译调用方之后出现具有相同签名的派生非虚拟方法,则CLR仍将调用C编译器选择的"固定"方法。这是为了避免脆弱的基类问题。

如果需要动态方法分辨率,请使用virtual。如果你不使用virtual,你会得到完全的静态分辨率。你的选择。成为this指针的对象引用的运行时类型在非虚拟方法的解析中根本不重要(对于csc.exe和clr jit都不重要)。

JIT将始终调用精确选择的方法。如果方法不存在,它将引发异常(可能是因为被调用方dll已更改)。它不会调用其他方法。

callvirt也可以调用非虚拟方法。它用于执行空检查。它是以这种方式定义的,并且C被定义为对每个调用执行空检查。


正如@usr在类似问题中回答的那样,我发布了如何解决非虚拟实例方法继承问题?:

Runtime usually means"when/everytime the code runs". The JIT
resolution here is only involved once before the code runs. What the
JIT does is not being referred to by saying"at runtime".

也用杰弗里的话说

JIT compiler locates the type object that corresponds to the type of
the variable being used to make the call.

我认为这里的变量类型是指"由元数据令牌指定的类"(ECMA 335 III.3.19调用),JIT基于该类解析方法目标。

C编译器总是找出要调用的正确方法,并将该信息放入元数据标记中。因此,JIT从不需要"沿着类层次结构走下去"。(但如果手动将元数据标记更改为继承的方法,则可以)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    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()的话,应用程序运行良好。


根据我的理解,并使用您的示例:引擎盖下:

基类中的虚拟方法将在派生类方法表中有一个条目。这意味着"object"类型中的所有虚拟方法在其所有派生类方法表中都可用。

在派生类中没有提供功能的非虚拟方法(如示例代码中所示)实际上不会在派生类方法表中有条目!

为了检查这个,我在windbg中运行了代码来检查manager类的方法表。

methoddesc表条目methodde jit名称

506A4960 503A6728 prejit system.object.toString()。

50698790 5036730 prejit system.object.equals(system.object)

50698360 5036750 prejit system.object.gethashcode()。

506916F0 503A6764 prejit system.object.finalize()。

001B00C8 00143904 JIT管理器..ctor()

0014c065 001438f8无管理器.genprogressreport()

因此,我可以看到对象的虚拟对象方法,但是我看不到实际的getYearsEmployed方法,因为它不是虚拟的,并且没有派生的实现。顺便说一下,同样的概念,在派生类中也看不到someothermethod函数。

但是,您可以调用这些函数,只是它们不在方法表中。我可能是不正确的,但我相信是通过调用堆栈找到它们的。也许这就是里克特先生在书中的意思。我觉得他的书很难读,但那是因为概念很复杂,他比我聪明。)

我不确定IL是否反映了问题。我相信它可能是在IL下面的一层,这就是我为什么用windbg来查看的原因。我想你可以用windbg看到它在堆栈中移动……