1. 程式人生 > >C# 中的 delegate, Lambda 表示式 和 event

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#的協變和逆變的特性,詳細參考:

委託中的協變和逆變)。delegate 是引用型別,所以如果沒有為 sample 初始化引用例項,那麼 sample 的值是 null。

現在有一個問題。如果 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