對於上一篇隨筆,不少朋友留下了很不錯的見解,也有不少朋友提出了很有代表性的問題。所以,在正文開始之前,我想先就這些問題闡述一下自己的觀點,也請朋友們給予批評指正:
首先來說,feiyang朋友提出了有關委托與函數指針間的連帶關系。我很贊同這樣的說法。
其實,對C++有所了解的朋友都會知道:一個類內部所包含的方法,其實在類內部僅僅表現為一個函數指針,其實現部分並未占用類的內存空間。假如我們在一個類內部聲明了一個委托實例對象,同時為它掛載了其他類內部的一個方法(當然,這個方法必須是共有型的),這樣,在本類內部就同樣擁有了這個方法的函數指針(可以理解為它被存儲在委托對象的內部)——這就相當於在本類內部聲明了一個一模一樣的方法。
而事實上,假如我們通過已聲明的委托對象來調用相應的方法,C#的編譯器將自動將其作為本類的聲明方法來對待(從C#編譯器的安全角度來論證,這個說法是行得通的)。其實目標委托對象並非真正意義上的類方法,而僅僅只是作為一個目標方法的代理方法而存在,這便是委托(代理)一詞的由來。
另外feiyang兄也提到了有關委托與接口回調機制的區別與聯系問題,我已經在上一篇隨筆中給予了回復,其中談到了有關接口回調方法所存在的一些限制。在這裡,我們不妨嘗試一下,使用接口回調是否可以實現C#的事件機制:
5(2)使用接口回調仿真控件的事件機制(嘗試)
首先,我們建立一個控件項目,代碼如下:
namespace InfceCalbckEvCtrl
{
public partial class UserControl1 : UserControl
{
//聲明接口類型
public interface ICallbackEvn
{
void ShowObjTxt(string Txt);
}
//定義(但不實例化)接口對象
public ICallbackEvn ObjCallEvn;
public UserControl1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
ObjCallEvn.ShowObjTxt("接口回調仿真事件調用成功!");
}
}
}
而後,我們同樣構建一個上層的Demo程序來調用它:
namespace InCallDemo
{
public partial class Form1 : Form, InfceCalbckEvCtrl.UserControl1.ICallbackEvn
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
//接口對象實例化
userControl11.ObjCallEvn = new Form1();
}
public void ShowObjTxt(string Txt)
{
MessageBox.Show(Txt);
}
}
}
需要大家留心的地方我已經用紅筆標明了:
1> 首先Form1類不但要繼承自Form類,同時還必須繼承InfceCalbckEvCtrl.UserControl1控件類中的ICallbackEvn接口——這就是所謂的繼承關系的限制。
2> 同時也只能是利用接口來默認掛載本類中被命名為void ShowObjTxt(string Txt)的方法,如果大家仍然想為目標接口掛載另外一個方法(當然這個方法不能再以void ShowObjTxt(string Txt)來命名了)來實現其他的功能,那麼對不起,請您重新定義原有的接口好了——這就是所謂方法同名的限制(其實這裡的限制不單單只有同名,還包括了同方法數量的限制)。
同時,我稍稍查閱其它網友有關回調所貼的一些帖子,個人感覺使用委托的人數比使用接口來完成回調的人數要多。
以上是我的觀點,歡迎大家給予批評指針,也請大家盡量發表自己的看法,謝謝。呵呵……
以下是有關委托的一些稍稍復雜的用法舉例:
6.使用委托數組
namespace ObjFunDelegate
{
delegate double CompuFun(double parama,double paramb);
class Program
{
static void Main(string[] args)
{
//聲明委托數組
CompuFun[] computefun = new CompuFun[2];
//為數組中的多個對象分別掛載不同的方法
computefun[0] = new CompuFun(DeleMathFuns.AddFun);
computefun[1] = new CompuFun(DeleMathFuns.MultiplyFun);
//通過下標分別調用不同委托對象中的不同方法
for (int i = 0; i < 2; i++)
{
Console.WriteLine("調用方法[{0}]:", i);
Console.WriteLine("所得結果:{0}", computefun[i](5, 5));
}
Console.ReadLine();
}
}
class DeleMathFuns
{
public static double AddFun(double a, double b)
{
return a + b;
}
public static double MultiplyFun(double a, double b)
{
return a * b;
}
}
}
如果說通過最初的幾段代碼還不能論證我先前的觀點的話,相信這段代碼便可以將委托的對象性質以及方法性質兼備的特性淋漓盡致的表現出來:委托對象中掛載的方法我們可以隨時隨地方便的調用,同時我們又可以按照普通數組的管理方法來管理這多個委托對象。使我們的程序有條不紊的運行。
不過在C#中,多個方法可不一定非得要對應多個委托對象呢。呵呵……
7.多路廣播委托
namespace ObjFunDelegate
{
delegate void CompuFun(double parama,double paramb);
class Program
{
static void Main(string[] args)
{
CompuFun computefun;
computefun = new CompuFun(DeleMathFuns.AddFun);
computefun += new CompuFun(DeleMathFuns.MultiplyFun);
computefun(5, 5);
Console.ReadLine();
}
}
class DeleMathFuns
{
public static void AddFun(double a, double b)
{
Console.WriteLine("AddFun方法調用結果為{0}", a + b);
}
public static void MultiplyFun(double a, double b)
{
Console.WriteLine("MultiplyFun方法調用結果為{0}", a * b);
}
}
}
這段代碼在第6段代碼的基礎上發生了小小的改動:為了說明問題方便我將委托的返回臨行改為void,同時在DeleMathFuns類相應的方法中直接輸出結果。比之第6段代碼你能發現什麼問題嗎?沒錯,現在是多個方法掛接到同一個委托對象上了。在委托對象被調用時,掛接於委托對象之上的方法會按照多個方法掛接的先後順序依次對其執行。這便是所謂的多路廣播委托。
在這裡我要強調一個問題,可用於多路廣播委托的方法返回值必須是void型,這也是為什麼我要在6的基礎上對7的委托返回類型加以改動的原因。道理很簡單,如果各個方法含有返回值,現在將其捆在一個委托對象之上,那麼在調用時返回值要送給誰呢。呵呵……
我們從C#中為什麼會引入委托開始探討,到這裡C#中委托的基本用法已經闡述完畢,下面,我們可以結合C#的一些高級論題對委托的功能進行一下更深層次的挖掘。
8.委托的跨線程操控
在此,我以人們常說的跨線程操控控件為例。首先,如果你已經掌握了C#多線程的基本用法,不妨自己試著實現一下這段程序。在未掌握委托之前,我曾經這樣寫過:
using System.Threading;
namespace MulTrdDelegate
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
//初始化子線程對象
private Thread demoThread = null;
private void button1_Click(object sender, EventArgs e)
{
demoThread = new Thread(new ThreadStart(ThreadProcUnsafe));
demoThread.Start();
}
public void ThreadProcUnsafe()
{
textBox1.Text = "這個控件的內容是由子線程實現的";
}
}
}
確定子線程創建無誤,編譯也通過了。但是,運行時卻出現了這樣的錯誤:
原來,C#中是不支持跨線程操控控件的。不過,出於安全考慮,這樣做也是合情合理,這樣可以避免多個線程同時操控一個控件所帶來的程序運行錯誤。那麼,如何來實現這樣的效果呢?
方法一:將 Control.CheckForIllegalCrossThreadCalls 屬性設置為 false。你可以嘗試給窗體添加Form_Load()事件,然後在事件函數體中對這個屬性進行設置。
編譯就會發現,程序已經可以順利執行了。這個方法比較簡單,但始終不太安全。所以我們不推薦這樣的用法。
方法二:用委托來實現
using System.Threading;
namespace MulTrdDelegate
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
//定義委托類型
public delegate void TreadDelgate();
//初始化子線程對象
private Thread demoThread = null;
private void button1_Click(object sender, EventArgs e)
{
demoThread = new Thread(new ThreadStart(ThreadProcUnsafe));
demoThread.Start();
}
public void ThreadProcUnsafe()
{
SetText();
}
private void SetText()
{
string text = "這個控件的內容是由子線程實現的";
if (this.textBox1.InvokeRequired)
{
TreadDelgate Objdelegate = new TreadDelgate(SetText);
this.Invoke(Objdelegate, new object[] {});
}
else
{
this.textBox1.Text = text;
}
}
}
}
編譯運行,你會發現程序順利的通過了。對比上一段代碼,應該很容易發現,這一段代碼在上一段代碼的基礎上新增了委托的定義,再就是關鍵的SetText()方法。
那麼,大家不妨來分析一下這個方法的代碼內容:
TreadDelgate Objdelegate = new TreadDelgate(SetText); 這一句當然是聲明委托對象實例。
比較難理解的可能是 this.textBox1.InvokeRequired ,當目標控件textBox1將要被其他的子線程操控時,其相應的InvokeRequired 屬性值即為真。
另外一句this.Invoke(Objdelegate, new object[] {}); 查一下MSDN文檔,它的意思是:在擁有控件的基礎窗口句柄線程上,用指定的參數列表執行指定的委托。
看明白意思了吧,呵呵…… 這幾句代碼所實現得功效貫穿下來就可以描述為——如果目標控件textBox1 將要被其他線程操控,就自動創建主線程的委托,並用它掛載SetText()方法,然後以子線程控制主線程端以委托執行相應的方法。
在這裡,我想闡述一下自己的一點見解:
如果你試著在SetText()方法中下一個斷點,同時跟蹤一下代碼的運行方式的話,便不難發現SetText()方法的調用順序是這樣的:程序首先進入if模塊,而後憑借委托重新對方法進行調用,當再次進入方法的調用時,程序會直接跳過if模塊而直接進入else模塊。我認為,這裡應該涉及到一個方法相對於線程的歸屬問題:
根據屬性Control. InvokeRequired在MSDN中的解釋,我們可以獲悉,第一次調用SetText()方法時,C#的編譯器實際上是將這個方法作為隸屬於子線程的一個方法來看待,所以,this.textBox1.InvokeRequired的返回值才為true。而在此之後,程序跳過了if模塊而直接進入else模塊,說明此時SetText()方法被作為主線程的方法調用。
其實,對於已經創建好的方法,其本身無所謂隸屬於誰,哪個線程調用,它便可被看做是誰的方法。這種情況與方法的跨類調用不同 —— 因為一個方法隸屬於哪個類本是一目了然的事情。在這裡,你可以稍稍回想一下我們在前面得出的一個很關鍵的結論:C#中的委托實例能夠非常靈活的兼容對象與方法的特質——我認為這是上述技術得以實現的關鍵所在!
在C#中,要人為控制一個方法隸屬於哪個線程不太容易,然而,要控制一個對象屬於某一個固定的線程卻很簡單——只要讓目標對象在這個線程裡被創建就可以了!這就可以解釋為什麼上述代碼要憑借委托來實現了:既然我們不能控制一個方法隸屬於某一個線程,那麼便不妨通過控制委托這種特殊的對象來間接控制掛載於它上面的方法和相關線程的隸屬關系!這便實現了如何安全的通過委托進行方法的跨線程調用。
我們再回過頭來看,事實上,SetText()方法第一次被調用時確實被看做是子線程的方法,因為是子線程調用了它,然而,第二次的情況不同,顯然,方法的第二次調用觸發時,依然是子線程相對占據統治地位,並且恰恰是它控制委托對象對目標方法進行了第二次調用。然而該方法卻是被主線程中創建的委托對象調用的,自然而然也就隸屬於主線程了。這論證了先前我關於“假如我們通過已聲明的委托對象來調用相應的方法,C#的編譯器將自動將其作為本類的聲明方法來對待(從C#編譯器的安全角度來論證,這個說法是行得通的)”這個觀點,只不過這裡的編譯器安全性已不再是針對類對象,而是針對線程對象而言了。
這樣一來,討論的中心再度回歸到“委托(實例化對象)兼具對象與方法二者的特性”這個主題上來:委托實例本身就是以對象的形式而存在的,同時它又可以掛接方法,使其本身可以靈活調用這些方法——也就是說,借助委托,我們實現了方法的對象化。(未完待續)