標籤:style blog http color io os ar 使用 sp
在開始之前,先說一下文章的表達習慣。
Object a = new Object();
在上面的例子裡,Object 是一種類型,a 是一個引用,new Object() 構造了一個對象,有時 ”構造對象“ 也被稱為 ”建立執行個體“。有的文章習慣把 a 也稱作執行個體,請根據上下文理解不要混淆。接下來你會經常看到類似 ”Object 類型“ ”Object 引用“ ”Object 對象“ 這樣的表達。
另外,我會盡量使用 ”方法“ 而不使用 ”函數“ 的稱呼。習慣使用 C/C++ 的讀者接下來看到 ”方法“ 時不要感到困惑。
delegate 簡介
delegate 的中文意思是委託,在 c# 裡也是一個關鍵字。在 System 命名空間裡,有 Delegate 類 和 MulticastDelegate 類,後者繼承前者。編譯器和其他工具可以從 MulticastDelegate 派生,但是我們無法顯式的去繼承這兩個類。為了使用 delegate,我們需要使用 delegate 關鍵字來聲明一個 delegate 類型:
public delegate int MyDelegate(int arg);
和其他類型一樣,這個聲明可以放在一個命名空間內部或其他類的內部。我們只需要寫這樣一行代碼,編譯器為我們實現細節。注意 delegate 關鍵字後面的部分,可以是任何合理的方法簽名(方法簽名由方法的參數列表和傳回值組成,這兩者都相同的方法具有相同的簽名)。
現在我們已經聲明了一個 delegate 類型 MyDelegate,接下來用這個類型建立一個執行個體:
MyDelegate sample = new MyDelegate(Method);
看起來和建立其他類的執行個體沒有什麼區別,但是建構函式的參數 Method 是什麼呢?Method 必須是與 MyDelegate 聲明時的簽名一致的方法名稱,也就是說 Method 的簽名必須含有一個 int 參數和 int 傳回值(這樣描述並不準確,由於C#的協變和逆變的特性,詳細參考:委託中的協變和逆變)。delegate 是參考型別,所以如果沒有為 sample 初始化引用執行個體,那麼 sample 的值是 null。
現在有一個問題。如果 Method 是靜態方法,那麼這樣傳遞參數完全沒有問題;如果 Method 不是靜態方法,這樣就不一定正確了。我們知道普通方法必須由具體的對象來發起調用,所以傳遞一個普通方法給 delegate 的建構函式而不指定所屬對象是沒有道理的。如果上面這段代碼在類的普通方法裡,由於 this 關鍵字可以省略,所以 Method 被編譯器理解為 this.Method,所屬對象是明確的,所以仍是正確的;如果上面這段代碼在類的靜態方法裡,那麼會因為 Method 沒有明確的所屬對象而錯誤。這個細節需要留意。
最後我們需要瞭解如何調用 delegate 執行個體裡儲存的方法。
int a = sample(3);
看起來與直接調用 Method 沒有什麼區別。的確如此,僅需要注意 delegate 引用如果為 null 則會引起Null 參考異常。delegate 引用與其他類型的引用一樣,還可以執行 = 賦值運算、作為方法的參數傳遞、使用點運算子 . 訪問對象成員。
目前為止,您可能覺得 delegate 與 C/C++ 裡的函數指標很相似。但是,delegate 類型還可以執行 +/+= 和 -/-= 操作,因為 delegate 對象並不只是儲存一個方法的引用,而是儲存了一個方法列表。
sample -= Method;sample += Method;
- 用於從方法列表裡移除一個方法,+ 用於增加一個方法。+ 操作不會排除完全相同的方法引用。現在,sample(3) 的意義是依次調用方法列表裡的每個方法,最後一個方法的傳回值作為 sample(3) 的傳回值。
請注意類型轉換問題。在上面的代碼裡,我們把方法與 delegate 相加。其實,還可以直接把方法賦值給 delegate 引用。這些做法之所以合理,是因為方法可以隱式轉化為 delegate。
為了簡化編程,System 命名空間裡預聲明了一系列的泛型 delegate。使用這些聲明我們幾乎不再需要做額外的聲明工作了。
public delegate void Action();public delegate void Action<T>(T arg);public delegate void Action<T1, T2>(T1 arg1, T2 arg2);// 還有更多參數版本的 Actionpublic delegate TResult Func<TResult>();public delegate TResult Func<T, TResult>(T arg);public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);// 還有更多參數版本的 Func
匿名方法和 Lambda 運算式
在上文中,我們必須事先聲明 Method 方法才能初始化 delegate 執行個體。C# 提供了匿名方法來簡化 delegate 的初始化工作。
MyDelegate sample = delegate(int arg){ return arg * arg;};
看起來好像在運行代碼內部聲明了一個新方法並使用 sample 儲存。這樣做使得代碼更簡潔。注意匿名方法的傳回型別,必須是或可以隱式轉換為 delegate 聲明時簽名的傳回型別。匿名方法還有一個好處是可以訪問外部變數,匿名方法捕獲外部變數的引用而不是值,這一點需要留意,請看這段代碼:
int n = 0;sample = delegate(int arg) { return n * arg; };n = 1;print(sample(1));
結果將輸出 1 而不是 0。另外匿名方法裡無法訪問外部的 ref 或 out 參數。
Lambda 運算式是一種匿名函數。在 Lambda 運算子 => 的左邊指定輸入參數,在右邊指定執行運算式或語句。
sample = x => x * x;
看起來與上文的匿名方法非常相似。實際上匿名方法的所有限制和特徵在 lambda 運算式中同樣適用。
先看 Lambda 運算式的輸入參數部分:
// 無參數使用()() => DoSomeThing();// 一個參數直接寫出參數名arg => arg * arg;// 多個參數使用,分割(arg1, arg2) => arg1 == arg2;// 參數類型無法推斷時可以顯式指定(string arg1, char arg2) => arg1.Split(arg2);
在這些例子裡,=> 的右側都是單一運算式,如果需要傳回值,那麼運算式的值就是傳回值。使用大括弧可以書寫多條語句,但必須使用 return 明確指定傳回值。
(arg1, arg2) =>{ if(arg1 == arg2) return 0; return arg1 > arg2 ? 1 : -1;}
event 簡介
event 中文意思是事件,在 c# 裡也是一個關鍵字。如下是 event 的聲明方式:
public event MyDelegate myEvent{ add { print("add: " + value); } remove { print("remove: " + value); }}
看起來與屬性的聲明相似。event 具有兩種操作:+= 和 -=,右運算元必須為與 event 類型一致的 delegate(或 null)。+= 調用的是 add,-= 調用的是 remove。下面是調用的樣本:
myEvent += null;myEvent -= null;
結果列印出 "add: " 和 "remove: "。
有一個更簡單的聲明方法:
public event MyDelegate myEvent;
這種聲明類似於(實際並不相同):
private MyDelegate _myEvent;public event MyDelegate myEvent{ add { _myEvent += value; } remove { _myEvent -= value; }}
使用這種簡單的聲明方法,你可以在 myEvent 所在的類中像對 delegate 一樣對 event 進行操作,包括 = 賦值運算和調用方法;但是在其他地方,你只能使用 += 和 -= 運算。這樣做的目的是,把所有可能不安全的操作封裝在類內部。對就是這樣,沒有更多了。
最後,文章的目的是儘可能用簡單的語言把委託和事件描述清楚,因此忽略了所有的非同步作業等相關細節。更細節的資料請參考 msdn 或這篇文章:C# in Depth: Delegates and Events。
【C#】delegate 和 event