C# 中的 delegate, Lambda 表示式 和 event
在開始之前,先說一下文章的表達習慣。
Object a = new Object();
在上面的例子裡,Object 是一種型別,a 是一個引用型別的變數,new Object() 構造了一個物件,構造物件也被稱為建立例項。有的文章習慣把 a 也稱作例項,請根據上下文理解不要混淆。接下來你會經常看到型別, 引用, 物件這些詞彙。
另外,我會使用"方法"而不使用"函式"的稱呼。習慣使用 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#的協變和逆變的特性,詳細參考:
現在有一個問題。如果 Method 是靜態方法,那麼這樣傳遞引數完全沒有問題;如果 Method 不是靜態方法,這樣就不一定正確了。我們知道類的成員方法必須由具體的物件來發起呼叫,所以傳遞一個普通方法給 delegate 的建構函式而不指定所屬物件是沒有道理的。如果上面這段程式碼在類的普通方法裡,由於 this 關鍵字可以省略,所以 Method 被編譯器理解為 this.Method,所屬物件是明確的,所以仍是正確的;如果上面這段程式碼在類的靜態方法裡,那麼會因為 Method 沒有明確的所屬物件而錯誤。這個細節需要留意。
最後我們需要了解如何呼叫 delegate 例項裡儲存的方法。
int a = sample(3);
看起來與直接呼叫 Method 沒有什麼區別。的確如此,僅需要注意 delegate 引用如果為 null 則會引起空引用異常。delegate 引用與其他型別的引用一樣,還可以執行 = 賦值運算、作為方法的引數傳遞、使用點運算子 . 訪問物件成員。
目前為止,您可能覺得 delegate 與 C/C++ 裡的函式指標很相似。但是,delegate 型別還可以執行 +/+= 和 -/-= 操作,因為 delegate 物件並不只是儲存一個方法的引用,而是儲存了一個方法列表。
sample -= Method;
sample += Method;
- 用於從方法列表裡移除一個方法,+ 用於增加一個方法。+ 操作不會排除完全相同的方法引用。現在,sample(3) 的意義是依次呼叫方法列表裡的每個方法,最後一個方法的返回值作為 sample(3) 的返回值。
請注意型別轉換問題。在上面的程式碼裡,我們把方法與 delegate 相加。其實,還可以直接把方法賦值給 delegate 引用。這些做法之所以合理,是因為方法可以隱式轉化為 delegate(這種操作和 string 的工作方式相似,你無法修改 string 型別的物件本身,但可以在執行 +/+= 時建立新的 string 物件並返回給當前引用)。
為了簡化程式設計,System 名稱空間裡定義了一系列的泛型 delegate:
public delegate void Action();
public delegate void Action<T>(T arg);
public delegate void Action<T1, T2>(T1 arg1, T2 arg2);
// 還有更多引數版本的 Action
public 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("[hello] " + value(3)); }
remove { print("[world] " + value(7)); }
}
看起來與屬性的定義相似。event 具有兩種操作:+= 和 -=,右運算元必須為與 event 定義型別一致的 delegate 例項(或 null)。+= 呼叫的是 add,-= 呼叫的是 remove。下面是呼叫的示例:
myEvent += x => x;
myEvent -= x => x;
結果打印出 "[hello] 3" 和 "[world] 7"。
有一種簡化版的定義方法:
public event MyDelegate myEvent;
這種定義本質類似於:
private MyDelegate _sample;
public event MyDelegate myEvent
{
add { _sample += value; }
remove { _sample -= value; }
}
使用上面的這種簡單版的定義方法,由於沒有可見的 _sample,那麼對 MyDelegate 例項的訪問就成了問題。為了解決這個問題,C# 是這樣設計的:你可以在 myEvent 所在的類中像對 _sample 一樣對 myEvent 進行各種操作,包括 = 賦值運算和呼叫儲存的方法;但是在其他地方,myEvent 就是標準的 event,你只能使用 += 和 -= 運算。這樣做的目的是,把所有可能不安全的操作封裝在類內部。對就是這樣,沒有更多了。
最後,文章的目的是儘可能用簡單的語言把委託和事件描述清楚,因此忽略了所有的非同步操作等相關細節。更細節的資料請參考 msdn 或這篇文章:C# in Depth: Delegates and Events。