關於參考型別,虛方法的調用比較直接簡單,直接看這個類:
class a
{
public virtual void doo()
{ }
}
class b : a
{
public override void doo()
{
base.doo();
}
}
非常簡單,b繼承a,改寫a的doo方法,並且又調用基類a的doo方法。看b的doo方法的IL:
.method public hidebysig virtual instance void doo() cil managed
{
.maxstack 8
L_0000: nop
L_0001: ldarg.0
L_0002: call instance void Mgen.a::doo()
L_0007: nop
L_0008: ret
}
可以看到,在類方法中調用基類的虛方法(或者其他方法),直接用call指令。後面的instance代表非靜態方法。
接著在主函數中這樣做:
new b().doo();
((a)new b()).doo();
即通過b和a分別調用一個b對象的doo方法:
L_0000: nop
L_0001: newobj instance void Mgen.b::.ctor()
L_0006: callvirt instance void Mgen.a::doo()
L_000b: nop
L_000c: newobj instance void Mgen.b::.ctor()
L_0011: callvirt instance void Mgen.a::doo()
L_0016: nop
不難,呵呵。上面兩句指令完全是等價的,所以IL都一樣。由於這個虛函數是定義在類a中的,所以子類調用永遠等效於使用callvirt的a::doo()函數。
參考型別的虛函數調用在IL上是比較直接的,在實值型別的調用上會稍有些複雜,下面來看實值型別的。
實值型別不能進行繼承操作,所有實值型別隱式繼承System.ValueType,所以實值型別不能定義虛函數,我們看這個結構體:
struct MyStruct
{
public override string ToString()
{ return null; }
/* 沒有改寫GetHashCode方法 */
}
我們改寫了Object.ToString()虛函數,但是沒有改寫GetHashCode虛函數。
接下來,主函數代碼:
MyStruct ms = new MyStruct();
ms.ToString();
ms.GetHashCode();
ms.GetType();
分別調用了3個函數:
- ToString:改寫基類的虛函數。
- GetHashCode:未改寫的基類虛函數。
- GetType:基類非虛函數。
IL:
//初始化:ms = new MyStruct();
L_0000: nop
L_0001: ldloca.s ms
L_0003: initobj Mgen.MyStruct
//ToString
L_0009: ldloca.s ms
L_000b: constrained. Mgen.MyStruct
L_0011: callvirt instance string [mscorlib]System.Object::ToString()
L_0016: pop
//GetHashCode
L_0017: ldloca.s ms
L_0019: constrained. Mgen.MyStruct
L_001f: callvirt instance int32 [mscorlib]System.Object::GetHashCode()
L_0024: pop
//GetType
L_0025: ldloc.0
L_0026: box Mgen.MyStruct //裝箱
L_002b: call instance class [mscorlib]System.Type [mscorlib]System.Object::GetType()
L_0030: pop
先來看非虛函數GetType,這個函數是在Object類中定義的,而實值型別在棧中的,不能像堆中的參考型別那樣隨心所欲得調用繼承來的成員,必須先通過裝箱才可以的。所以調用一個實值型別的GetType,實際上CLR會在堆中建立一個裝箱的對象,然後調用這個對象的GetType。
事實上,調用未改寫的虛函數也是一樣的,所以上例中調用GetHashCode應該需要裝箱的,但是GetHashCode和ToString的IL是一樣的(當然除了函數簽名),都是用的callvirt,然後對應函數是System.Object類的成員。原因就是上面那個constrained指令。這個constrained專門用在callvirt之前,來看MSDN的解釋:
http://msdn.microsoft.com/zh-cn/library/system.reflection.emit.opcodes.constrained.aspx
當 callvirt method 指令前面帶有首碼 constrained thisType,該指令將按照以下步驟執行:
如果 thisType 為參考型別(相對於實值型別),則 ptr 被取值 (Dereference),並作為“this”指標傳遞到 method的 callvirt。
如果 thisType 為實值型別,而且 thisType 實現 method,則 ptr 作為“this”指標在不作任何修改的狀態下傳遞到 call 。 method 指令,以便 thisType 實現 method。
如果 thisType 為實值型別,而且 thisType 不實現 method,則將取消對 ptr 的引用,對它進行裝箱,然後將它作為“this”指標傳遞到 callvirt 指令。 method指令。
原來constrained指令會根據類型確保後面的callvirt會調用成功,所以上面的GetHashCode函數可以直接用callvirt而不用box IL指令,因為有了constrained指令,CLR會自動在執行callvirt時對實值型別變數進行裝箱。而上面的ToString方法,加不加constrained都無所謂,因為實值型別已經重寫了Object的ToString方法,callvirt總會成功的。