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

前提是你 提交于 2019-12-03 05:47:44

The C# compiler resolves non-virtual methods exactly with no wiggle room. If a derived non-virtual method with the same signature appearch after the caller was compiled, the CLR will still call the "fixed" method the C# compiler chose. This is to avoid the brittle base class problem.

If you want dynamic method resolution, use virtual. If you don't use virtual you get fully static resolution. Your choice. The runtime type of the object reference becoming the this pointer does not matter in resolution of non-virtual methods at all (neither for csc.exe not for the CLR JIT).

The JIT will always call the exactly chosen method. It will throw an exception if the method does not exist (maybe because the callee DLL was changed). It will not call a different method.

callvirt can also call non-virtual methods. It is used to perform a null check. It is defined that way, and C# is defined to perform a null check on every call.

From my understanding, and using your example: Under the hood:

A VIRTUAL method in a base class WILL have an entry in a derived class method table. This means that all the virtual methods in the 'object' type are available in all their derived classes method table.

A NON virtual method (as in the example code), with no supplied functionality in the derived classes will NOT actually have an entry in the derived classes method tables!

To check this, I ran the code in WinDbg to examine the method table for the Manager class.

MethodDesc Table Entry MethodDe JIT Name

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()

So,I can see the virtual object methods of object, but I can't see the actual method GetYearsEmployed since it's not virtual and has no derived implementation. Incidentally, by the same concept, you can't see the SomeOtherMethod function in the derived class either.

You can, however, call these functions, it's just they are not there in the method table. I could be incorrect, but I believe the call stack is walked to find them. Maybe this is what Mr Richter means in his book. I find his book difficult to read but that's because the concepts are complicated and he is cleverer than me :)

I'm not sure the IL reflects the problem. I believe it's possibly a layer below IL which is why I've used Windbg to take a look. I suppose you could use windbg to see of it walks the stack....

colinfang

As answered by @usr in the similar question I posted How is non-virtual instance method inheritance resolved?:

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".

Also in Jeffrey's words

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

The variable type here I believe means "the class specified by the metadata token" (ECMA 335 III.3.19 call) based on which JIT resolves the method destination.

C# compiler always figures out the correct method to call, and put that info into the metadata token. So JIT never has to "walk down the class hierarchy". (But it can if you manually change the metadata token to an inherited method)

    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

If we use Ildasm + Ilasm to change A::Foo() to B::Foo(), and to change A::Bar() to B.Bar(), the application runs fine.

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!