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

C# 中定義 Event 的標準流程


1. 實作一個  System.EventArgs 的子代, 強烈建議設計成 immutable. C# 建議名字最好叫做 XxxEventArgs, 例如:

public class NewMailEventArgs : EventArgs { ...; }

2. 於發送端 class 定義一個 event member 如下 (不一定要 public), 根據命名規範, 應該要大寫開頭

public class MailManager {
    public event EventHandler<NewMailEventArgs> NewMail;
}

3. 於發送端 class 定義一個 protected virtual method 如下:

public class MailManager {
    protected virtual void OnNewMail(NewMailEventArgs e) {
        EventHandler<NewMailEventArgs> temp = this.NewMail;
        if(temp != null) temp(this, e);
    }
    ...
}

4. 當事件發生時, 呼叫上述的 protected virtual method

-------------------------------------------------------------------------
重點:
步驟 3 之所以要切成兩半, 是為了防止多緒時的 race condition. 因為 delegate 是 immutable, 故只需如步驟 3 所做就可以達成防止 race condition 的需求.

不過步驟 3 有一個問題: 若 compiler 夠聰明, 它可能會將步驟 3 優化如下:
    protected virtual void OnNewMail(NewMailEventArgs e) {
        if(this.newMail != null) this.NewMail(this, e);
    }

CLR 為了保證回溯相容性, 故不會做這種優化. 但為了防止未來版本的 CLR 做此優化, 寫 .NET 4.0 以上的 C# 程式時可以把步驟 3 改成這樣:

public class MailManager {
    protected virtual void OnNewMail(NewMailEventArgs e) {
        EventHandler<NewMailEventArgs> temp = Volatile.Read(ref this.NewMail);
        if(temp != null) temp(this, e);
    }
    ...
}

-------------------------------------------------------------------------
步驟三其實是可以被公式化的, 以下是透過 class extension 來對步驟 3 做公式化的方法:

public static class EventArgExtensions {
    public static void Raise<T>(this T e, Object sender, ref EventHandler<T> eventDelegate) {
        EventHandler<T> temp = Volatile.Read(ref eventDelegate);
        if(temp != null) temp(sender, e);
    }
}

於是原來的步驟三可以寫成這樣:
protected virtual void OnNewMail(NewMailEventArgs e) {
    e.Raise(this, ref this.NewMail);
}

-------------------------------------------------------------------------
EventHandler 其實是一個 delegate, 宣告如下:
public delegate void EventHandler<T>(Object sender, T e);

-------------------------------------------------------------------------
C# Compiler 其實會把下面這宣告

public class MailManager {
    public event EventHandler<NewMailEventArgs> NewMail;
}

偷偷的改寫為這樣:

public class MailManager {
    private EventHandler<NewMailEventArgs> NewMail = null;
    public void add_NewMail(EventHandler<NewMailEventArgs> value) {
        ...; // 將 value 以 thread-safe 的方式加入 this.NewMail
    }
    public voie remove_NewMail(EventHandler<NewMailEventArgs> value){
        ...; // 將 value 以 thread-safe 的方式自 this.NewMail 中移除
    }
}

注意!!! add_XXX 以及 remove_XXX 的宣告修飾詞會與當初 event 的修飾詞一樣. 如果當初的 event 是 virtual 則 compiler 產生出來的這兩個 methods 就會是 virtual. 其他如 protected, private static 亦然.

-------------------------------------------------------------------------
以下是一個事件接收端的案例:
public class Fax {
    public Fax(MailManager mm)  {
        mm.NewMail += this.FaxMsg;
    }
    public void Unregister(MailManager mm) {
        mm.NewMail -= this.FaxMsg;
    }
    private void FoxMsg(Object sender, NewMailEventArgs e) {
        ...; // 處理之
    }
}

上面的 mm.NewMail += this.FaxMsg; 其實會被 compiler 改為:
mm.add_NewMail(new EventHandler<NewMailEventArgs>(this.FaxMsg));

mm.NewMail -= this.FaxMsg; 亦會被 compiler 改為:
mm.remove_NewMail(new EventHandler<NewMailEventArgs>(FaxMsg));



出處: CLR via C# 4Ed Chapter 11

IEnumerable/IEnumerator 的兩三事


以下是一個傳統且結實的 Enumerable & Enumerator 範例:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;

namespace ConsoleApplication1 {
    class Program {
        class MyEnumerable : IEnumerable <int >, IEnumerable {
            private int[] values;
            private int offset;
            public MyEnumerable( int[] values, int offset) {
                this.values = values;
                this.offset = offset;
            }
            public IEnumerator<int > GetEnumerator() {
                return new MyIterator(this );
            }
            IEnumerator IEnumerable .GetEnumerator() {
                return this.GetEnumerator();
            }
            private class MyIterator : IEnumerator <int >, IEnumerator {
                private MyEnumerable target;
                private int pos;
                public MyIterator( MyEnumerable target) {
                    this.target = target;
                    this.pos = -1;
                }
                public bool MoveNext() {
                    if( this.pos != this.target.values.Length) this.pos++;
                    return this.pos < this.target.values.Length;
                }
                public int Current {
                    get {
                        if( this.pos == -1 || this.pos == this.target.values.Length) {
                            throw new InvalidOperationException();
                        }
                        int index = this.pos + this.target.offset;
                        index = index % this.target.values.Length;
                        return this.target.values[index];
                    }
                }
                object IEnumerator.Current {
                    get {
                        return this.Current;
                    }
                }
                public void Reset() { this.pos = -1; }
                void IEnumerator.Reset() { this .Reset(); }
                public void Dispose() { }
            }
        }
        static void Main( string[] args) {
            MyEnumerable list = new MyEnumerable (new int [] {1,2,3,4,5,6}, 2);
            foreach( int i in list) Console.WriteLine(i);
            Console.WriteLine( "-------------------------");
            foreach( object o in (( IEnumerable)list)) Console .WriteLine(o);
            Console.WriteLine( "-------------------------");
            IEnumerator<int > iterator = list.GetEnumerator();
            while(iterator.MoveNext()) Console.WriteLine(iterator.Current.ToString());
            (iterator as IDisposable).Dispose();
            Console.ReadKey();
        }
    }
}

自從 C# 2.0 之後, 上面的程式可以被簡化成下面這樣

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;

namespace ConsoleApplication1 {
    class Program {
        class MyEnumerable : IEnumerable <int >, IEnumerable {
            private int[] values;
            private int offset;
            public MyEnumerable( int[] values, int offset) {
                this.values = values;
                this.offset = offset;
            }
            public IEnumerator<int > GetEnumerator() {
                for( int index = 0; index < values.Length; index++) {
                    yield return values[(index + this.offset) % this.values.Length];
                }
            }
            IEnumerator IEnumerable .GetEnumerator() {
                return this.GetEnumerator();
            }
        }
        static void Main( string[] args) {
            MyEnumerable list = new MyEnumerable (new int [] {1,2,3,4,5,6}, 2);
            foreach( int i in list) Console.WriteLine(i);
            Console.WriteLine( "-------------------------");
            foreach( object o in (( IEnumerable)list)) Console .WriteLine(o);
            Console.WriteLine( "-------------------------");
            IEnumerator<int > iterator = list.GetEnumerator();
            while(iterator.MoveNext()) Console.WriteLine(iterator.Current.ToString());
            (iterator as IDisposable).Dispose();
            Console.ReadKey();
        }
    }
}

實際上發生的事情是: 
當 Compiler 看到 yield return 時,
它會自動地幫我們產生一個 Enumerator class,
就像我們之前自己寫的那樣!!!
不過, 有幾個注意事項, 首先觀察下面程式碼的輸出:

using System;
using System.Collections.Generic;

namespace ConsoleApplication1 {
    class Program {
        private static readonly String padding = new String( ' ', 30);
        private static IEnumerable<int > CreateEnumerable() {
            Console.WriteLine( "{0}Start of CreateEnumerable()" , padding);
            for( int i = 0; i < 3; i++) {
                Console.WriteLine( "{0}About to yield {1}", padding, i);
                yield return i;
                Console.WriteLine( "{0}After yield", padding);
            }
            Console.WriteLine( "{0}End of CreateEnumerable()", padding);
        }
        static void Main( string[] args) {
            IEnumerable<int > iterable = CreateEnumerable();
            IEnumerator<int > iterator = iterable.GetEnumerator();
            Console.WriteLine( "Starting to iterate");
            while( true) {
                Console.WriteLine( "Calling MoveNext()...");
                bool result = iterator.MoveNext();
                Console.WriteLine( "... MoveNext result={0}", result);
                if(!result)
                    break;
                Console.WriteLine( "Fetching Current...");
                Console.WriteLine( "... Current result={0}", iterator.Current);
            }
            Console.ReadKey();
        }
    }
}

因為 Compiler 動了很大的手腳, 
所以上面那隻程式的輸出會讓人很意外!!!
有興趣的話可以使用 Reflector 或 ILDasm 來觀察 Compiler 究竟做了甚麼事.

使用 yield return 來產生 Enumerator/Enumerable 時,
有幾個特別的事項要提醒:
1. code 會在第一次 MoveNext() 時才被執行
2. code 裏頭可以有 try 以及 finally 區塊, 但不可以有 catch 區塊
3. finally 區塊實際上會被移至 Enumerator 的 Dispose() 裡
4. 在第一次 MoveNext() 被呼叫前, Current 會是 default(T)
5. 當 MoveNext() 回傳 false 後, Current 永遠會是最後值
6. 呼叫 Reset() 會丟例外

出處: C# In Depth 2Ed Chapter 7

interface 的兩三事


CLR 要求 interface 裡面的所有 methods/properties/indexers/delegates/events  都必須是 virtual.
因此, 下面幾行程式:

class Base : IDisposable, ICloneable {
    public Object Clone() { ...; }
    public virtual void Dispose() { ...; }
}

其實會被 C# Compiler 偷偷改成下面這樣:

class Base : IDisposable, ICloneable {
    public virtual sealed Object Clone() { ...; }
    public virtual void Dispose() { ...; }
}

-------------------------------------------------------
當一個 type 被 load 進 CLR, CLR 會在記憶體內建立一個 type object, 用來記錄該 type 的資訊, 其中也包含 method table. 

method table 內包含:
1. new methods introduced by the type
2. any virtual methods inherited by the type

比如說:
class SimpleType : IDisposable {
    public void Dispose() { ...; }
}

SimpleType 的 type object 內的 method table 會含有:
1. 所有 Object 定義的 virtual methods
2. IDispose 定義的 virtual method - void Dispose();
3. SimpleType 自建的新 function - void DIspose();

以上的 2 和 3 是兩個欄位, 並不是我寫錯. 當 C# Compiler 發現 3 和 2 其實可以參考同一個 method 時, 會將他們填入相同的值. 參考下面的 code

SimpleType st = new SimpleType();
st.Dispose(); // 呼叫的是 3
((IDisposable)st).Dispose(); // 呼叫的是 2

以下的語法叫做 Explicit Interface Method Implementation (EIMI), 可以讓上述的 2 與 3 填入不同的 method.

class SimpleType : IDisposable {
    public void Dispose() { // 填入欄位 3
        Console.WriteLine("SimpleType.Dispose");
    }
    void IDisposable.Dispose() { // 填入欄位 2
        Console.WriteLine("IDispose.Dispose");
    }
}

請注意一件很重要的差別!!! Dispose() 其實是一個全新的 function, 跟 IDisposable 一點關係也沒有, 只是恰巧也叫做 void Dispose() 而已. IDisposable.Dispose() 才是真正實作 IDisposable 的 method. 他的修飾詞會被強迫偷偷的設定為 private virtual sealed. 只有將 SimpleType 明確轉型為 IDisposable 時才呼叫得到.

出處: CLR via C# 4Ed Chapter 13

Type Constructor

Type Constructor (或稱 static constructor) 並不會在程式啟動時馬上就被呼叫. 而是在

1. 任何該 Type 的 static properties/methods/fields 第一次被存取時
2. 任何該 Type 的 non-static methods/properties/indexers/constructors 第一次被呼叫時

由 JIT Compiler 在呼叫點之前【植入】額外的程式碼來呼叫 Type Constructor. 

CLR 保證:
1. Type Constructor 在每一個 AppDomain 只會被呼叫一次
2. Type Constructor 是 thread-safe 的

CLR 不保證:
1. Type Constructor 被呼叫的順序

若希望 CLR 執行 Type Constructor, 可參考下列程式碼
namespace ConsoleApplication1 {
    class A {
        static A() { Console.WriteLine("static A()"); }
    }
    class Program {
        static void Main(string[] args) {
            System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof(A).TypeHandle);
            Console.ReadKey();
        }
    }
}

但請注意!!! CLR 絕對不會在同一個 AppDomain 裡呼叫同一個 TypeConstructor 兩次, 也不保證 TypeConstructor 之間的呼叫順序.因此上面那個 RunClassConstructor 若使用第二次的話是完全無作用的.

-----------------------------------------------------------------------------
對於這個 class 
class Dummy {
    private static Int32 val = 5;
}

表面上他並沒有宣告 type constructor, 
但為了初始化 val,
C# Compiler 其實會偷偷的幫它產生一個 type constructor.

出處: CLR via C# 4Ed Chapter 8