2013年7月26日 星期五

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

關於常數的兩三事

定義常數時,若該數值永遠不可能改變則使用 const,否則建議使用 readonly。

假設某 A.DLL 內含一 class 如下:
public sealed class Consts {
    public static readonly Int32 const1 = 50;
    public const Int32 const2 = 50;
}

某程式 B 使用了 A.DLL 內的 Consts,如下:
public void Test()  {
    Console.WriteLine(Consts.const1);
    Console.WriteLine(Consts.const2);
}

這個函式會印出
50
50

一段時間之後,A.DLL 發行新版本,Consts 被改成這樣:
public sealed class Consts {
    public static readonly Int32 const1 = 10;
    public const Int32 const2 = 10;
}

若程式 B 沒有 recompile,而使直接使用新版的 A.DLL 的話,輸出結果會是:
10
50

這是因為,const 會直接鑲進目標程式的 code 裡(效能好),readonly 則是會執行期才去取值(效能較差)。B 程式其實等價於:
public void Test()  {
    Console.WriteLine(Consts.const1);
    Console.WriteLine(50);
}

因此除非 B 做 recompile,否則 A.DLL 對於 Consts.const2 的改變不會被程式 B 看見。

出處: CLR via C# 4Ed Chapter 7

Identity And Equality

先定義兩個名詞:
Identity - 兩個變數是否參考到同一個物件
Equality - 兩個變數參考到的物件的值是不是相等

System.Object.Equals() 基本上實做的是 Identity,他的 code 大概長這樣:

public class Object {
    public virtual Boolean Equals(Object rhs) {
        if(this == rhs) return true;
        return false;
    }
}

由於子代有可能 override System.Object.Equals() 以及各種比較算符 (== 或 !=),因此若要判斷兩個變數是否參考到同一個物件 (Identity),請不要使用 Equals() 或 ==,請使用 Object.ReferenceEquals(),他的 code 大概長這樣:

public class Object {
    public static Boolean ReferenceEquals(Object lhs, Object, rhs) {
        return (lhs == rhs);
    }
}

System.ValueType 是 System.Object 的子代,它 override 了 Equals() 這個 method,讓它變成實作 Equality。但由於 System.ValueType.Equals() 是靠 reflection 來實作 Equals(),因此它的效率很差。因此當程式設計師設計了自己的 Value Type,而且該 ValueType 有比較的需求時,最好提供一個 override 的 Equals() method。

大部份的 class 都沒有比較 Equality 的需求,因此不太需要 override Equals()。但 Value Type 通常會有 override Equals() 的需求。當您要 override Equals() 時,請注意下列事項:

* Reflexive -> x.Equals(x) 必須是 true
* Symmetric -> 若 x.Equals(y), 則 y.Equals(x) 必須為 true
* Transitive -> 若 x.Equals(y) 且 y.Equals(z),則 x.Equals(z) 必須為 true
* Consistent -> 只要物件沒被改變,兩次 Equals() 的結果應該要一樣。

以下是通常要做的額外項目:
* 您應該順便繼承及實作 System.IEquatable<T>
* 您應該要 override GetHashCode(),並且保證:
    # 若兩個物件 Equals(),他們的 GetHashCode() 的回傳值必須相同。
    # 只要物件未被更改,兩次 GetHashCode() 的回傳值必須相同。
* 您應該順便實作 == 算符與 != 算符
* 如果您的 type 可以被排序:
    # 您應該繼承及實做 System.IComparable
    # 您應該繼承及實做 System.IComparable<T>
    # 您應該實做所有的比大小算符,包含 <, <=, >, >=

以下是一個 override Equals 的範例. 為求簡單, 我只專注在 Equals 以及 GetHashCode 這兩個 method 上, 請務必仔細研讀!!!

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

namespace ConsoleApplication1 {
    class Program {
        class Base {
            private Int32 val;
            public Base(Int32 val) { this.val = val; }
            // 因為父類別是 Object, 他的 Equals 其實是檢查 identity
            // 所以在這裡不應該呼叫
            public override bool Equals(object obj) {
                if(obj == null) return false;
                if(Object.ReferenceEquals(this, obj)) return true;
                if(this.GetType() != obj.GetType()) return false;
                if(this.val != ((Base)obj).val) return false;
                return true;
            }
            // 因為父類別是 Object, 
            // 他的 GetHashCode 其實是透過 this 指標的位址求得,
            // 故在這裡不應該呼叫
            public override int GetHashCode() {
                return this.val.GetHashCode();
            }
        };
        class Drived : Base {
            private Int32 val;
            public Drived(Int32 valOfDrived, Int32 valOfBase) : base(valOfBase) {
                this.val = valOfDrived;
            }
            // 請注意!!! 父類別是否是 System.Object 會影響到 Equals 的實作方式
            public override bool Equals(object obj) {
                if(!base.Equals(obj)) return false;
                if(this.val != ((Drived)obj).val) return false;
                return true;
            }
            // 請注意!!! 父類別是否是 System.Object 會影響到 GetHashCode 的實作方式
            public override int GetHashCode() {
                return base.GetHashCode() ^ this.val.GetHashCode();
            }
        }
        static void Main(string[] args) {
            Base b = new Base(1);
            Console.WriteLine(b.Equals(new Base(1)));
            Console.WriteLine(b.Equals(new Base(2)));
            Console.WriteLine(b.Equals(new Drived(2, 1)));
            Console.WriteLine(b.Equals(b));
            Console.WriteLine(b.Equals(null));
            Console.WriteLine("------------------------------");
            Drived d = new Drived(2, 1);
            Console.WriteLine(d.Equals(new Base(1)));
            Console.WriteLine(d.Equals(new Drived(1, 1)));
            Console.WriteLine(d.Equals(new Drived(2, 1)));
            Console.WriteLine(d.Equals(d));
            Console.WriteLine(d.Equals(null));
            Console.ReadKey();
        }
    }
}


出處: CLR via C# 第四版第五章