在前兩篇文章中,我們逐步探討了委托的基本概念、多路廣播以及事件機制的本質。讀完第一篇後,許多開發者心中可能已浮現一個念頭:委托不就是函數指標嗎?筆者早期也有此猜測,但缺乏文獻佐證。這系列文章實為筆者對委托機制的探索歷程,旨在透過常見用法層層遞進,幫助同樣對委托感到困惑的開發者少走彎路。
近期偶然發現木頭象大神的文章《從函數指針到代理(C#代理入門)》,文中指出「委托是函數指標功能的擴展」。這一觀點頗具啟發性,姑且不論其對錯,我們可以此為切入點,探討委托相較於傳統函數指標究竟擴展了哪些功能。
在 C# 中了解對象特質極為便捷:聲明對象後輸入「.」號,智能感知即會列出其屬性與方法。對實例化的委托對象同樣適用,透過此方式可快速掌握委托特有的方法與屬性。以下重點解析幾個關鍵方法的應用。
異步委托主要運用 BeginInvoke() 與 EndInvoke() 兩個方法。相較於同步執行的 Invoke(),它們具備以下特性:
BeginInvoke() 配對使用,用於檢索調用結果。若異步方法尚未完成,則會阻塞直至執行完畢。其參數為 BeginInvoke() 返回的 IAsyncResult 對象。以下範例展示基本異步調用流程:
using System.Threading;
namespace AsyncDelegate
{
class Program
{
delegate void MyDelegate();
static void Main(string[] args)
{
MyDelegate ObjDelegate = new MyDelegate(ObjDelegateFun);
// 異步調用
IAsyncResult ObjIr = ObjDelegate.BeginInvoke(null, null);
Console.WriteLine("委托方法執行中
.");
Thread.Sleep(5000);
ObjDelegate.EndInvoke(ObjIr);
Console.ReadLine();
}
static private void ObjDelegateFun()
{
Thread.Sleep(5000);
Console.WriteLine("委托方法調用成功!");
}
}
}
執行時會發現「委托方法執行中...」幾乎瞬間顯示,證明 BeginInvoke() 非阻塞返回。主線程休眠 5 秒後呼叫 EndInvoke() 獲取結果,完成異步調用。
透過 AsyncCallback 實現回調模式,避免主線程主動等待:
using System.Threading;
namespace AsyncDelegate
{
class Program
{
delegate void MyDelegate();
static void Main(string[] args)
{
MyDelegate ObjDelegate = new MyDelegate(ObjDelegateFun);
// 指定回調方法
IAsyncResult ObjIr = ObjDelegate.BeginInvoke(new AsyncCallback(CallbackFun), ObjDelegate);
Console.WriteLine("委托方法執行中
.");
Console.ReadLine();
}
static private void ObjDelegateFun()
{
Thread.Sleep(5000);
Console.WriteLine("委托方法調用成功!");
}
static private void CallbackFun(IAsyncResult ar)
{
MyDelegate ObjDelegate = (MyDelegate)ar.AsyncState;
ObjDelegate.EndInvoke(ar);
Console.WriteLine("回調方法執行完畢!");
Console.ReadLine();
}
}
}
此處使用 BeginInvoke() 的另一個重載,傳入回調委托與狀態對象。目標方法完成後自動觸發回調,在回調中呼叫 EndInvoke() 獲取結果。
異步委托機制與多線程有異曲同工之妙,實際執行時目標方法的線程號確與主線程不同。但經查閱資料(推薦 Kuffy Wang 的《異步委托與多線程的區別》),兩者存在細微差異:異步不等於單純開啟子線程,同步亦非專指單線程環境。
關於上一篇遺留的 Control.Invoke() 問題,筆者總結如下關鍵差異:
| 方法 | 完成工作 | 執行線程 |
|---|---|---|
| Control.Invoke | 完成工作 | 強制於 UI Thread |
| Control.BeginInvoke | 立即返回 | 強制於 UI Thread |
| [delegate].Invoke | 完成工作 | 呼叫 Invoke 的 Thread |
| [delegate].BeginInvoke | 立即返回 | 新的背景 Thread |
關鍵在於:無論委托或 Control,Invoke() 皆為「絕對同步」,BeginInvoke() 皆為「絕對異步」。即使在多線程環境下,同步方法會強制按單線程方式執行;異步方法則強制按多線程方式執行。
以經典的跨線程更新 UI 為例,若直接在子線程中操作控件會拋出異常。安全做法是透過委托配合 Control.Invoke() 將操作封送至 UI 線程:
public void ThreadProcUnsafe()
{
SetText();
}
private void SetText()
{
string text = "這個控件的內容是由子線程實現的";
if (this.textBox1.InvokeRequired)
{
TreadDelgate Objdelegate = new TreadDelgate(SetText);
this.Invoke(Objdelegate, new object[] {});
MessageBox.Show("委托方法已返回!");
}
else
{
this.textBox1.Text = text;
}
}
執行時會發現 MessageBox.Show 在 SetText() 返回後才執行,證明 Control.Invoke() 為絕對同步。若改用 this.BeginInvoke() 則行為與 [delegate].BeginInvoke() 一致。
筆者認為,委托實例兼具「對象」與「方法」雙重特性,是實現跨線程安全調用的關鍵。我們無法直接控制方法隸屬於哪個線程,但可透過控制委托對象(在特定線程建立)間接控制掛載方法的線程歸屬。
委托的核心優勢在於「兼具對象與方法二者特質」:既可作為對象傳遞、操控,又可直接調用掛載的方法。這系列探討旨在拋磚引玉,期望讀者能總結出適合自身思路的委托使用場景,看見屬於你眼中的委托真正面貌。