今天和同事在讨论一个CLR的问题,题目如下:
{
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();
}
}
{
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):
{
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()
{
}
}
}
3. 若调用的是虚实例方法,CLR执行如下步骤:
(1) 根据发起调用的变量,找到变量所引用的托管堆上的类型实例对象
(2) 根据类型实例对象中的 "类型对象指针字段" 找到类型对象.
(3) 在类型对象中找到所调用的方法地址,或向基类回溯。
(4) 执行调用。
例如(例2):
{
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()
{
}
}
}
但是我们看一下下面的例子(例3):
{
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()
{
}
}
}
上面这个例子貌似不再满足之前说的对虚实例函数的调用的调用方式。按照之前的规则应该是两次都调用类型B的Print方法。
这里有两个细节:
1. 该代码用new关键字隐藏了从A继承的Print方法,我理解为在B的实例中是调不到A中定义的Print方法的。
2. 如何标识调用的目标是哪个方法。
将上述Main函数的代码编译为IL:
{
.entrypoint
// Code size 43 (0x2b)
.maxstack 2
.locals init ([ 0] class TestConsole.Program/A b,
[ 1] class TestConsole.Program/B actualB,
[ 2] bool 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
可能我会说,这好像和之前说的第三条规则是矛盾的,Print是虚函数,但是第一次调用却是调用的类型的PrintA.
那我们再看一下将B听Print方法改用override修改编译出的IL:
{
.entrypoint
// Code size 43 (0x2b)
.maxstack 2
.locals init ([ 0] class TestConsole.Program/A b,
[ 1] class TestConsole.Program/B actualB,
[ 2] bool 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:
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:
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的方法
这样再结合三条规则,结果就很容易理解了。