網上講C#委托和事件
的博文已經非常多了,其中也不乏一些深入淺出、條理清晰的文章。我之所以還是繼續寫,主要是借機整理學習筆記、歸納總結從而理解更透徹,當然能夠以自己的理解和思路給其他人講明白更好。
另外,太長的文章會讓很多讀者失去興趣,所以我決定把這篇分成四個部分來介紹。分別是委托的基礎、委托的進階、事件的基礎和事件的進階。對使用委托與事件要求不高的同學可以跳過進階部分。
本文接著講委托的高級知識,上一節請參見C#委托與事件(1)。
4. 委托的高級知識
(1) 委托的類組成
前面我們介紹過了,委托實際上也是一個類,只不過它的對象不是一個普通的變量,而是一個方法。但是我們實際使用時并不需要定義這個類,而只是聲明一下委托即可。這是因為當C#
編譯器處理委托類型時,會自動產生一個派生自System.MulticastDelegate
的密封類,這個類與它的基類System.Delegate
一起為委托提供必要的基礎設施。
下面我們就以上一節中聲明的委托delegate void GreetingDelegate(string s);
來看看該類的組成。
public sealed class GreetingDelegate : System.MulticastDelegate
{
public void Invoke(string s);
public IAsyncResult BeginInvoke(string s, AsyncCallback cb, object state);
public void EndInvoke(IAsyncResult result);
}
可以看到,該類中定義了三個公共方法:
(a) Invoke()
,它被用來以同步方式調用委托對象維護的每個方法。所謂同步是指調用者必須等待調用完成才能繼續執行。Invoke()
方法定義的參數和返回值完全匹配我們要定義的類GreetingDelegate
。另外,Invoke()
不能直接調用,而是在后臺調用。
(b) BeginInvoke()
,用于異步調用,它最前面的參數列表是GreetingDelegate
定義的方法類的參數列表,此外,還有兩個參數AsyncCallback
和object
用于異步方法調用。
(c) EndInvoke()
,與BeginInoke()
聯合用于異步調用,它的返回值與委托聲明的返回值一致,而它的唯一參數則是BeginInvoke()
返回的類型IAsyncResult
接口。
另外,上面我們定義的委托沒有返回值,也可以定義返回值類型,這樣Invoke()
和EndInvoke()
對應的返回值就不是void
了。委托還可以指向包含任意數量out
或者ref
參數的方法,按道理只有Invoke()
和BeginInvoke()
方法與委托的參數列表有關,需要加上相應的out
或者ref
參數,但是由于異步調用時需要通過EndInvoke()
來返回結果,所以EndInvoke()
的參數列表中需要加上out
或者ref
參數。
下面簡單介紹一下委托類的父類System.MulticastDelegate
。
public abstract class MulticastDelegate : Delegate
{
// 返回所指向的方法列表
public sealed override Delegate[] GetInvocationList();
// 重載等于和不等于操作符
public static bool operator ==(MulticastDelegate d1, MulticastDelegate d2);
public static bool operator !=(MulticastDelegate d1, MulticastDelegate d2);
// 用來在內部管理委托所維護的方法列表
private IntPtr _invocationCount;
private object _invocationList;
}
public abstract class Delegate : IConeable, ISerializable
{
// 與函數列表交互的方法
// 給委托維護的列表添加一個方法,在C#中使用重載+=操作符調用此方法
public static Delegate Combine(params Delegate[] delegates);
public static Delegate Combine(Delegate a, Delegate b);
// 從調用列表中移除一個或所有的方法,在C#中使用-=操作符調用此方法
public static Delegate Remove(Delegate source, Delegate value);
public static Delegate RemoveAll(Delegate source, Delegate value);
// 重載操作符
public static bool operator ==(Delegate d1, Delegate d2);
public static bool operator !=(Delegate d1, Delegate d2);
// 擴展委托目標的屬性
public MethodInfo Method { get; } //用以表示委托維護的靜態方法的詳細信息
public object Target { get; } // 如果方法調用是定義在對象級別的,
// Target返回表示委托維護的方法的對象;
// 如果調用的方法時一個靜態成員,
// Target返回null
}
(2) 泛型委托
C#
允許我們定義泛型委托,即當我們定義的委托接受的參數可能會不同時,我們可以通過類型參數來構建。下面我們來改寫一下上一節中定義的那個委托,使得它不僅支持傳入string
,還支持傳入整型。
namespace TestDelegate
{
delegate void GreetingDelegate<T>(T arg);
class Program
{
public static void Hello(string s)
{
Console.WriteLine(" Hello, {0}!", s);
// do something (hug or shake hand...)
}
public static void Goodbye(string s)
{
Console.WriteLine(" Goodbye, {0}!", s);
// do something (hug or wave hand...)
}
public static void GreetingTimes(int n)
{
Console.WriteLine(" Greeting {0} times!", n);
}
static void MakeGreeting<T>(T name, GreetingDelegate<T> greeting)
{
greeting(name);
}
static void Main(string[] args)
{
GreetingDelegate<string> d1 = Hello; //定義委托的一個對象(將方法綁定到委托)
d1 += Goodbye; // 在d1上再綁定一個委托
GreetingDelegate<int> d2 = GreetingTimes; //定義委托的另一個對象
MakeGreeting("April", d1);
MakeGreeting(99, d2);
}
}
}
輸出內容:
Hello, April!
Goodbye, April!
Greeting 99 times!
有了泛型委托,很多方法都可以用一個委托模板表示出來,因此C#
中提供了兩個常用的泛型委托Action<T>
和Func<T>
來避免用戶手工構建自定義委托的麻煩。
Action<T>
Action<T>
泛型委托定義的方法,參數列表可以多至16個(使用時需要指定各個參數的類型),返回值為void
。
例如,我們有一個方法為static void DisplayMessage(string msg, ConsoleColor txtColor, int printCount)
,若把它作為Action<T>
委托的一個目標,則委托的實例化時需要這樣寫:
Action<string, ConsoleColor, int) actionTarget = new Action<string, ConsoleColor, int>(DisplayMessage);
調用委托方法時:
actionTarget("your input string", ConsoleColor.Green, 2);
Func<T>
Func<T>
泛型委托也可以指向多至16個參數的方法,但與Action<T>
不同的是,它具有自定義的返回值,具體用法類似,不再贅述。
(3) 委托的異步調用
在說委托的異步調用之前,我們先對第一節最早的delegate
例子做一個簡單的改進,看看它的工作流程。首先,MakeGreeting()
方法中除了調用greeting()
之外,需要再調用一個額外的方法FuncAfterGreeting()
。然后,我們定義了GreetingDelegate
的一個對象d1
,并在d1
上綁定了Hello()
和Goodbye()
方法。最后我們調用MakeGreeting()
方法來看看輸出結果。
namespace AsyncDelegateTest
{
delegate void GreetingDelegate(string arg);
class Program
{
public static void Hello(string s)
{
Console.WriteLine(" Hello, {0}!", s);
Console.WriteLine(" Waiting for 1 second");
Thread.Sleep(1000);
Console.WriteLine(" Finished Hello");
}
public static void Goodbye(string s)
{
Console.WriteLine(" Goodbye, {0}!", s);
Console.WriteLine(" Waiting for 2 second");
Thread.Sleep(2000);
Console.WriteLine(" Finished Goodbye");
}
public static void FuncAfterGreeting()
{
Console.WriteLine(" Do some other things...");
Console.WriteLine(" Waiting for 3 second");
Thread.Sleep(3000);
Console.WriteLine(" Finished FuncAfterGreeting");
}
static void MakeGreeting(string name, GreetingDelegate greeting)
{
greeting(name); // 這里相當于是greeting.Invoke(name);
FuncAfterGreeting();
}
static void Main(string[] args)
{
GreetingDelegate d1 = Hello; //定義委托的一個對象(將方法綁定到委托)
d1 += Goodbye; //定義委托的另一個對象
MakeGreeting("April", d1);
}
}
}
輸出結果:
Hello, April!
Waiting for 1 second
Finished Hello
Goodbye, April!
Waiting for 2 second
Finished Goodbye
Do some other things...
Waiting for 3 second
Finished FuncAfterGreeting
從輸出我們可以看到MakeGreeting()
方法中,首先調用了Hello()
方法,并運行完畢;然后調用了Goodbye()
方法,并運行完畢;最后調用FuncAfterGreeting()
,并運行完畢;至此,整個MakeGreeting()
方法運行完畢。
這就是采用同步的方式調用委托,這樣委托對象綁定的每個方法要依次執行,而且后者必須等前者執行完畢之后才能開始執行。另外,只有把委托對象綁定的所有方法執行完畢后才能回到MakeGreeting()
方法中繼續往下執行。
而在(1)中我們介紹的BeginInvoke()
和EndInvoke()
函數能使委托實現異步調用,所謂異步調用,就是在上例中MakeGreeting()
方法中的線程去執行greeting
方法時利用線程池中的線程去實現調用,自己則繼續往下執行。有了BeginInvoke()
和EndInvoke()
這兩個函數后,異步調用就很簡單了,直接先用greeting
調用BeginInvoke()
函數,然后就可以做其他的事情,結束之間再調用EndInvoke()
即可。
namespace AsyncDelegateTest2
{
delegate void GreetingDelegate(string arg);
class Program
{
public static void Hello(string s)
{
Console.WriteLine(" Hello, {0}!", s);
Console.WriteLine(" Waiting for 3 second");
Thread.Sleep(3000);
Console.WriteLine(" Finished Hello");
}
public static void FuncAfterGreeting()
{
Console.WriteLine(" Do some other things...");
Console.WriteLine(" Waiting for 3 second");
Thread.Sleep(3000);
Console.WriteLine(" Finished FuncAfterGreeting");
}
static void MakeGreeting(string name, GreetingDelegate greeting)
{
IAsyncResult result = greeting.BeginInvoke(name, null, null);
FuncAfterGreeting();
greeting.EndInvoke(result);
}
static void Main(string[] args)
{
GreetingDelegate d1 = Hello; //定義委托的一個對象(將方法綁定到委托)
MakeGreeting("April", d1);
}
}
}
輸出結果:
Do some other things...
Hello, April!
Waiting for 3 second
Waiting for 3 second
Finished Hello
Finished FuncAfterGreeting
為什么我這里把Goodbye
方法去掉了,這是因為BeginInvoke()
只能在綁定了單個方法的delegate
上調用,如果我們在d1
上還綁定了其他方法,那么去調用BeginInvoke()
的時候會出現下面的異常:
Unhandled Exception: System.ArgumentException: The delegate must have only one target.
當然如果你一定要綁定多個方法這樣用的話,可以先通過GetInvocationList()
獲得綁定的方法列表,然后依次調用BeginInvoke()
方法。
namespace AsyncDelegateTest3
{
delegate void GreetingDelegate(string arg);
class Program
{
public static void Hello(string s)
{
Console.WriteLine(" Hello, {0}!", s);
Console.WriteLine(" Waiting for 3 second");
Thread.Sleep(3000);
Console.WriteLine(" Finished Hello");
}
public static void Goodbye(string s)
{
Console.WriteLine(" Goodbye, {0}!", s);
Console.WriteLine(" Waiting for 2 second");
Thread.Sleep(2000);
Console.WriteLine(" Finished Goodbye");
}
public static void FuncAfterGreeting()
{
Console.WriteLine(" Do some other things...");
Console.WriteLine(" Waiting for 3 second");
Thread.Sleep(3000);
Console.WriteLine(" Finished FuncAfterGreeting");
}
static void MakeGreeting(string name, GreetingDelegate greeting)
{
Delegate[] delArray = greeting.GetInvocationList();
foreach (var d in delArray)
{
var del = (GreetingDelegate)d;
IAsyncResult result = del.BeginInvoke(name, null, null);
}
FuncAfterGreeting();
}
static void Main(string[] args)
{
GreetingDelegate d1 = Hello; // 定義委托的一個對象(將方法綁定到委托)
d1 += Goodbye; // 定義委托的另一個對象
MakeGreeting("April", d1);
Console.ReadLine(); // 如果不加這行的話,很可能Hello方法還沒執行完程序就退出了,
// 因為我們沒有調用EndInvoke()去檢查它們的狀態
}
}
}
輸出結果:
Do some other things...
Hello, April!
Goodbye, April!
Waiting for 2 second
Waiting for 3 second
Waiting for 3 second
Finished Goodbye
Finished FuncAfterGreeting
Finished Hello
回到AsyncDelegateTest2
,這里其實還存在一個問題。那就是MakeGreeting()
方法中的這句話greeting.EndInvoke(result);
,如果Hello()
方法需要執行30s,那么3s后FuncAfterGreeting()
方法就執行完畢了,主線程執行到EndInvoke()
這句話。而這句話就相當于讓主線程一直去查詢Hello()
方法是否執行完畢。那么問題來了,能不能不要這么麻煩主線程,而是讓Hello()
方法執行完畢后自動告訴主線程呢?這就是異步回調。
namespace AsyncDelegateTest4
{
delegate void GreetingDelegate(string arg);
class Program
{
public static void Hello(string s)
{
Console.WriteLine(" Hello, {0}!", s);
Console.WriteLine(" Waiting for 5 second");
Thread.Sleep(5000);
Console.WriteLine(" Finished Hello");
}
public static void FuncAfterGreeting()
{
Console.WriteLine(" Do some other things...");
Console.WriteLine(" Waiting for 3 second");
Thread.Sleep(3000);
Console.WriteLine(" Finished FuncAfterGreeting");
}
static void MakeGreeting(string name, GreetingDelegate greeting)
{
IAsyncResult result = greeting.BeginInvoke(name, new AsyncCallback(FuncForCallBack), "AsycState:OK");
FuncAfterGreeting();
}
static void FuncForCallBack(IAsyncResult result)
{
// AsyncResult should using System.Runtime.Remoting.Messaging
GreetingDelegate handler = (GreetingDelegate)((AsyncResult)result).AsyncDelegate;
handler.EndInvoke(result);
Console.WriteLine(result.AsyncState);
}
static void Main(string[] args)
{
GreetingDelegate d1 = Hello;
MakeGreeting("April", d1);
Console.ReadLine();
}
}
}
輸出結果:
Do some other things...
Hello, April!
Waiting for 5 second
Waiting for 3 second
Finished FuncAfterGreeting
Finished Hello
AsycState:OK
這里我們定義了一個回調函數FuncForCallBack()
,這樣就不需要在MakeGreeting()
方法的最后顯示地去調用EndInvoke()
去檢查委托方法的執行狀態了。
參考文獻:
《精通C#》
張子陽的《C# 中的委托和事件》
Delegates Tutorial
也來說說C#異步委托
C#委托的異步調用