2013年7月26日 星期五

關於 call method 的兩三事


CLR 提供兩種不同的 IL 指令來呼叫 method
--------------------------------------------------------------------------
[call]
call 可以用來呼叫所有型態的 method.
透過 call 來呼叫 static method 時,
必須標明 class, 例如:
call void ConsoleApplication1.Sample::StaticMethod()

透過 call 來呼叫 instance 或 virtual method 時,
必須提供一個變數, 用來參考該物件, 例如:

.local init(
    [0] class ConsoleApplication1.Base base2,
    [1] int32 num
)
...
ldloc.0
call instance void ConsoleApplication1.Base::InstanceMethod()

CLR 會根據變數所宣告的型別 (而非被參考的物件的型別),
去找找看該型別有沒有定義該 method,
若無, 則會到他的父代去找, 直到找到為止. (或找不到就丟例外)

另外, call 不會去檢查該變數的值是否為 null.

--------------------------------------------------------------------------
[callvirt]
callvirt 可以用來呼叫 instance 或 virtual method,
但不能用來呼叫 static method.

透過 callvirt 來呼叫 instance 或 virtual method 時,
也必須提供一個變數, 用來參考該物件, 例如:

.local init(
    [0] class ConsoleApplication1.Base base2,
    [1] int32 num
)
...
ldloc.0
callvirt instance void ConsoleApplication1.Base::InstanceMethod()

callvirt 會先檢查該變數的值是不是 null (是的話直接丟例外),
如果被 call 的是 instance method,
則 callvirt 接下來的行為和 call 一模一樣.
但若被 call 的是 virtual method,
則會以被參考物件的實際型別為起點, 往父代找符合的 method.
--------------------------------------------------------------------------

參考下面的 code

using System;
using System.Text;

namespace ConsoleApplication1 {
    class Base {
        public void InstanceMethod() {
            Console.WriteLine( "Base.InstanceMethod()" );
        }
        public virtual void VirtualMethod() {
            Console.WriteLine( "Base.VirtualMethod()" );
        }
        public static void StaticMethod() {
            Console.WriteLine( "Base.StaticMethod()" );
        }
    };
    class Drived : Base {
        public new void InstanceMethod() {
            Console.WriteLine( "Drived.InstanceMethod()" );
        }
        public override void VirtualMethod() {
            Console.WriteLine( "Drived.VirtualMethod()" );
        }
        public new static void StaticMethod() {
            Console.WriteLine( "Drived.StaticMethod()" );
        }
    };
    class Program {
        static void Main( string[] args) {
            Base obj = new Drived();
            obj.InstanceMethod();
            obj.VirtualMethod();
            Drived.StaticMethod();
            Int32 i = 5;
            i.ToString();
        }
    }
}

以下是 Main 的 IL
.method private hidebysig static void Main(string[] args) cil managed
{
    .entrypoint
    .maxstack 1
    .locals init (
        [0] class ConsoleApplication1.Base base2,
        [1] int32 num)
    L_0000: newobj instance void ConsoleApplication1.Drived::.ctor()
    L_0005: stloc.0
    L_0006: ldloc.0
    L_0007: callvirt instance void ConsoleApplication1.Base::InstanceMethod()
    L_000c: ldloc.0
    L_000d: callvirt instance void ConsoleApplication1.Base::VirtualMethod()
    L_0012: call void ConsoleApplication1.Drived::StaticMethod()
    L_0017: ldc.i4.5
    L_0018: stloc.1
    L_0019: ldloca.s num
    L_001b: call instance string [mscorlib]System.Int32::ToString()
    L_0020: pop
    L_0021: ret
}

注意幾個令人意外的地方:

首先, C# 居然用 callvirt 來呼叫 InstanceMethod!!!
這是因為 C# Compiler 認為檢查變數是否為 null 是很重要的.

其次, Int32.ToString()是一個 virtual method,
他 override 的 Object.ToString(),
但 C# Compiler 卻使用 call 來呼叫他!!!
這是因為 Int32 並不能被繼承,
而且變數本身的型別就是 Int32,
因此使用 call 不會造成問題, 這算是一種最佳化的方法.

--------------------------------------------------------------------------
另外一種情況會使得 C# Compiler 使用 call 來呼叫 virtual method,
參考以下程式碼:

class Dummy {
    public override string ToString() {
        return base.ToString();
    }
}

他的 IL 長這樣:

.class auto ansi nested private beforefieldinit Dummy
    extends [mscorlib]System.Object
{
    .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
    {
        .maxstack 8
        L_0000: ldarg.0
        L_0001: call instance void [mscorlib]System.Object::.ctor()
        L_0006: ret
    }

    .method public hidebysig virtual instance string ToString() cil managed
    {
        .maxstack 8
        L_0000: ldarg.0
        L_0001: call instance string [mscorlib]System.Object::ToString()
        L_0006: ret
    }

}

注意 C# Compiler 使用 call 來呼叫 base.ToString().
理由是如果用 callvirt 的話會造成無窮遞迴.


出處: CLR via C# 4Ed Chapter 6

沒有留言:

張貼留言