《CLR via C#》读书笔记(3) -- .Net程序的运行模型 下

今天和同事在讨论一个CLR的问题,题目如下:

class Program
    {
        static void Main(string[] args)
        {
            B b = new B();
            Console.ReadKey();
        }

        public class A
        {
            public static void Print()
            {
                Console.WriteLine("A");
            }

            public A()
            {
                Print();
            }
        }

        public class B : A
        {
            public static void Print()
            {
                Console.WriteLine("B");
            }

            public B()
            {
                Print();
            }
        }

View Code
class Program
    {
        static void Main(string[] args)
        {
            B b = new B();
            Console.ReadKey();
        }

        public class A
        {
            public void Print()
            {
                Console.WriteLine("A");
            }

            public A()
            {
                Print();
            }
        }

        public class B : A
        {
            public new void Print()
            {
                Console.WriteLine("B");
            }

            public B()
            {
                Print();
            }
        }

问题是:以上代码在控制台上打出的结果是什么?

我的答案是:B B,原因是这里是B的实例,而在B的实例中Print方法将A中继承而来的给隐藏了。

但是用VS尝试后发现结果是打出了A B。这不禁让我感到自己对CLR的运行模型还是缺乏了解。今天再次深入学习一下CLR中方法调用的运行模型。
在上一篇笔记中已经说过JIT的故事了。这次需要深入学习的是JIT过程之前的CLR所做的事情。

当我们生成B的对象时,CLR在托管堆中其实创建了如下结构:

B类型实例中保存所有B类型和从基类型所继承的成员变量字段,而A类型对象和B类型对象则保存了类型A和类型B中所定义的方法的代码地址以及静态字段。
而当代码中调用某个类型的方法时,CLR所采用的是如下规则:
1. 若调用的是静态方法,CLR会直接在所调用的类型所对应的类型对象中找到方法地址,然后执行。
2. 若调用的是非虚实例方法,CLR会在发起调用的变量的类型所对应的类型对象中找到方法地址 (或向上回塑到基类) ,然后执行。
 例如(例1):

class Program
{
        static void Main(string[] args)
        {
            A b = new B();
            b.Print();

            B actualB = b as B;
            if (actualB != null)
                actualB.Print();
            
            Console.ReadKey();
        }

        public class A
        {
            public void Print()
            {
                Console.WriteLine("A");
            }

            public A()
            {
                
            }
        }

        public class B : A
        {
            public new void Print()
            {
                Console.WriteLine("B");
            }

            public B()
            {
                
            }
        }
}
结果是,先调用类型A的Print方法,再调用B的Print方法。因此结果为:A B


3. 若调用的是虚实例方法,CLR执行如下步骤:
(1) 根据发起调用的变量,找到变量所引用的托管堆上的类型实例对象

(2) 根据类型实例对象中的 "类型对象指针字段" 找到类型对象.

(3) 在类型对象中找到所调用的方法地址,或向基类回溯。

(4) 执行调用。

例如(例2):

class Program
    {
        static void Main(string[] args)
        {
            A b = new B();
            b.Print();

            B actualB = b as B;
            if (actualB != null)
                actualB.Print();
            
            Console.ReadKey();
        }

        public class A
        {
            public virtual void Print()
            {
                Console.WriteLine("A");
            }

            public A()
            {
                
            }
        }

        public class B : A
        {
            public override void Print()
            {
                Console.WriteLine("B");
            }

            public B()
            {
                
            }
        }
    }

两次调用都会调用类型B的Print方法。因为是B的实例,且Print是虚方法。


但是我们看一下下面的例子(例3):

class Program
{
        static void Main(string[] args)
        {
            A b = new B();
            b.Print();

            B actualB = b as B;
            if (actualB != null)
                actualB.Print();
            
            Console.ReadKey();
        }

        public class A
        {
            public virtual void Print()
            {
                Console.WriteLine("A");
            }

            public A()
            {
                
            }
        }

        public class B : A
        {
            public new void Print()
            {
                Console.WriteLine("B");
            }

            public B()
            {
                
            }
        }
}
输出结果为A B,

上面这个例子貌似不再满足之前说的对虚实例函数的调用的调用方式。按照之前的规则应该是两次都调用类型B的Print方法。

这里有两个细节:

1. 该代码用new关键字隐藏了从A继承的Print方法,我理解为在B的实例中是调不到A中定义的Print方法的。

2. 如何标识调用的目标是哪个方法。

将上述Main函数的代码编译为IL:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       43 (0x2b)
  .maxstack  2
  .locals init ([0class TestConsole.Program/A b,
           [1class TestConsole.Program/B actualB,
           [2bool CS$4$0000)
  IL_0000:  nop
  IL_0001:  newobj     instance void TestConsole.Program/B::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  callvirt   instance void TestConsole.Program/A::Print()
  IL_000d:  nop
  IL_000e:  ldloc.0
  IL_000f:  isinst     TestConsole.Program/B
  IL_0014:  stloc.1
  IL_0015:  ldloc.1
  IL_0016:  ldnull
  IL_0017:  ceq
  IL_0019:  stloc.2
  IL_001a:  ldloc.2
  IL_001b:  brtrue.s   IL_0024
  IL_001d:  ldloc.1
  IL_001e:  callvirt   instance void TestConsole.Program/B::Print()
  IL_0023:  nop
  IL_0024:  call       valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
  IL_0029:  pop
  IL_002a:  ret
// end of method Program::Main
可以看到,第一次调用是调用是 A::Print(),而第二次是  B::Print()。

可能我会说,这好像和之前说的第三条规则是矛盾的,Print是虚函数,但是第一次调用却是调用的类型的PrintA.

那我们再看一下将B听Print方法改用override修改编译出的IL:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       43 (0x2b)
  .maxstack  2
  .locals init ([0class TestConsole.Program/A b,
           [1class TestConsole.Program/B actualB,
           [2bool CS$4$0000)
  IL_0000:  nop
  IL_0001:  newobj     instance void TestConsole.Program/B::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  callvirt   instance void TestConsole.Program/A::Print()
  IL_000d:  nop
  IL_000e:  ldloc.0
  IL_000f:  isinst     TestConsole.Program/B
  IL_0014:  stloc.1
  IL_0015:  ldloc.1
  IL_0016:  ldnull
  IL_0017:  ceq
  IL_0019:  stloc.2
  IL_001a:  ldloc.2
  IL_001b:  brtrue.s   IL_0024
  IL_001d:  ldloc.1
  IL_001e:  callvirt   instance void TestConsole.Program/A::Print()
  IL_0023:  nop
  IL_0024:  call       valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
  IL_0029:  pop
  IL_002a:  ret
// end of method Program::Main
 

我们可以看到,第一次和第二次的IL指令都是调用A::Print方法。

也就是说IL中的A::Print()和B::Print()仅仅是用来标识代码所想要调用的方法的一个标识。而之前的三条调用规则是属于CLR的运行时行为。

CLR运行那三条规则去找到一个用IL中指定的标识所指定的方法。

当B继承并override了A中的Print方法,但是这个方法的标识依然为A::Print,

但是当B用new关键字隐藏了继承的Print主法,那么B中的Print的标识则变为B::Print


如果这样理解便可以解释例3中的行为了:
第一次调用是要调用A::Print这个方法。它是个虚方法,于是CLR从B的类型对象开始找,但是B中没有标识为A::Print的方法,因为被B::Print覆盖了。
于是向上回溯找到类型A中的A::Print方法。

第二次调用是是调用B::Print,这个方法是虚方法,因此直接调用类型B的Print方法。


说了这么多还没有解答开头提出来的问题:
其实可以理解为A,B构造函数中对Print的调用为this.Print,等价于类型B的变量调用Print方法。

再来看看编译出来的IL:

类A的构造函数IL:

.method public hidebysig specialname rtspecialname 
        instance void  .ctor() cil managed
{
  // Code size       17 (0x11)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
  IL_0006:  nop
  IL_0007:  nop
  IL_0008:  ldarg.0
  IL_0009:  callvirt   instance void TestConsole.Program/A::Print()
  IL_000e:  nop
  IL_000f:  nop
  IL_0010:  ret
// end of method A::.ctor

类B的构造函数IL:

.method public hidebysig specialname rtspecialname 
        instance void  .ctor() cil managed
{
  // Code size       17 (0x11)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  call       instance void TestConsole.Program/A::.ctor()
  IL_0006:  nop
  IL_0007:  nop
  IL_0008:  ldarg.0
  IL_0009:  call       instance void TestConsole.Program/B::Print()
  IL_000e:  nop
  IL_000f:  nop
  IL_0010:  ret
// end of method B::.ctor

类A的构造函数调用的是标识为A::Print的方法
而类B的构造函数调用的是标识为B::Print的方法

这样再结合三条规则,结果就很容易理解了。

posted @ 2012-11-22 17:29  self.refactoring  阅读(276)  评论(1编辑  收藏  举报