介紹.NET中的委派(一)

來源:互聯網
上載者:User
介紹.NET中的委派(一)
----微軟 .NET平台系列文章之四



回呼函數
回呼函數的確是至今為止最有用的編程機制之一。C運行時的qsort函數利用回呼函數對數組元素進行排序。在Windows中,回呼函數更是視窗過程,鉤子過程,非同步程序呼叫,以及目前Microsoft .NET架構所必需的,在整個回調過程中自始至終地使用回調方法。人們可以註冊回調方法以獲得載入/卸載通知,未處理異常通知,資料庫/視窗狀態修改通知,檔案系統修改通知,功能表項目選擇,完成的非同步作業通知,過濾一組條目等等。
在C/C++中,一個函數的地址就是記憶體位址。這個地址不會附帶任何其它賦加資訊,如函數的參數個數,參數類型,函數的傳回值類型以及這個函數的調用規範。簡言之,C/C++回呼函數不是型別安全的。
在.NET架構中,回呼函數所受到的重用與它在Windows非受控編程中一樣。不同的是在.NET架構中提供了一種叫委派(delegates)的型別安全機制。我們先來研究一下委派的聲明。下面的代碼展示了如何聲明,建立和使用委派:
//
using System;
using System.WinForms;// 在beta2版本中為:System.Windows.Forms
using System.IO;

class Set {
private Object[] items;

public Set(Int32 numItems) {
items = new Object[numItems];
for (Int32 i = 0; i < numItems; i++)
items[i] = i;
}

// 定義 Feedback,類型為delegate
// (注意: 這個類型在Set類中是嵌套的)
public delegate void Feedback(
Object value, Int32 item, Int32 numItems);

public void ProcessItems(Feedback feedback) {
for (Int32 item = 0; item < items.Length; item++) {
if (feedback != null) {
// 一旦指定了回調,便調用它們
feedback(items[item], item + 1, items.Length);
}
}
}
}


class App {
[STAThreadAttribute]
static void Main() {
StaticCallbacks();
InstanceCallbacks();
}

static void StaticCallbacks() {
// 建立一個Set對象,其中有五個項目
Set setOfItems = new Set(5);

// 處理項目,feedback=null
setOfItems.ProcessItems(null);
Console.WriteLine();

// 處理項目,feedback=Console
setOfItems.ProcessItems(new Set.Feedback(App.FeedbackToConsole));
Console.WriteLine();

// 處理項目,feedback =MsgBox
setOfItems.ProcessItems(new Set.Feedback(App.FeedbackToMsgBox));
Console.WriteLine();

// 處理項目,feedback = console AND MsgBox
Set.Feedback fb = null;
fb += new Set.Feedback(App.FeedbackToConsole);
fb += new Set.Feedback(App.FeedbackToMsgBox);
setOfItems.ProcessItems(fb);
Console.WriteLine();
}

static void FeedbackToConsole(
Object value, Int32 item, Int32 numItems) {
Console.WriteLine("Processing item {0} of {1}: {2}.",
item, numItems, value);
}

static void FeedbackToMsgBox(
Object value, Int32 item, Int32 numItems) {
MessageBox.Show(String.Format("Processing item {0} of {1}: {2}.",
item, numItems, value));
}

static void InstanceCallbacks() {
//建立一個Set對象,其中有五個元素
Set setOfItems = new Set(5);

// 處理項目,feedback=File
App appobj = new App();
setOfItems.ProcessItems(new Set.Feedback(appobj.FeedbackToFile));
Console.WriteLine();
}

void FeedbackToFile(
Object value, Int32 item, Int32 numItems) {

StreamWriter sw = new StreamWriter("Status", true);
sw.WriteLine("Processing item {0} of {1}: {2}.",
item, numItems, value);
sw.Close();
}
}
//

注意代碼最上面的Set類。假設這個類包含一組將被單獨處理的項目。當你建立Set對象時,將它要操縱的項目數傳遞給它的建構函式。然後建構函式再建立對象(Objects)數組並初始化每一個整型值。
另外,Set類定義了一個公用的委派,這個委派指出某個回呼函數的簽名。在這個例子中,委派Feedback 確定了一個帶三個參數的方法(第一個參數為Object,第二和第三個參數都是Int32類型)並且返回void。在某種意義上,委派很像C/C++中表示某個函數地址的類型定義(typedef)。
此外,Set類定義了一個公用方法:ProcessItems。這個方法有一個參數feedback——一個對委派Feedback 對象的引用。ProcessItems迭代遍曆所有的數組元素,並且針對每一個元素調用回調方法(由feedback變數指定哪一個會調方法),這個回調方法被傳遞,從而以不同的方式處理回調方法所傳遞的項目值,項目數量以及數組中的元素數目。可以看出回調方法能以它選擇的任何方式處理每一個項目。
使用委派調用靜態方法
StaticCallbacks方法示範了用各種不同方式的回調委派。這個方法首先構造一個Set對象,告訴它對象建立有五個對象元素的數組。然後調用ProcessItems,在第一個調用中,它的feedback參數為null。ProcessItems呈現一個方法,這種方法為每一個Set操縱的項目實現某種動作。在第一個例子中,因為feedback參數是null,在處理每一個項目時不調用任何回調方法。
第二個例子中建立了一個新的Set.FeedBack委派對象,這個委派對象是一個方法封裝器,允許方法的調用是經由這個封裝器間接調用。對於類型FeedBack的構造器來說,方法的名字(App.FeedBackConsole)被作為構造器的參數傳遞;這就表示方法被封裝。然後,從new操作符返回的引用被傳到ProcessItems。現在,當執行ProcessItems時,它會調用App類型的FeedbackToConsole方法處理集合中的每一個項目。FeedbackToConsole簡單地將一個串輸出到控制台,表示哪個項目被處理了以及這個項目的值是什麼。
第三個例子與第二個例子基本相同。唯一的差別是Feedback委派對象封裝的是另一個方法:App.FeedbackToMsgBox。這個方法建立一個串,用這個串表示哪個項目被處理以及這個項目的值是什麼。然後將這個串顯示在一個資訊框中。
第四個例子也是靜態調用的最後一個例子示範了如何將委派連結在一起形成一個鏈。在這個例子中,首先創鍵一個Feedback委派對象的引用變數fb,並將它初始化為null。這個變數指向委派鏈表的頭。Null值表示鏈表中沒有節點。然後,構造Feedback委派對象,由這個對象封裝對App FeedbackToConsole方法的調用。C#中,+=操作符被用於將對象添加到fb引用的鏈表中。Fb此時指向鏈表的頭。
最後,構造另一個Feedback委派對象,由這個對象封裝對App FeedbackToMsgBox方法的調用。C#中的+=操作符又一次被用於將對象添加到fb引用的鏈表中,並且fb被新的鏈表的頭更新。現在,當執行ProcessItems時,傳遞給它的是Feedback委派鏈表的頭指標。在ProcessItems內部,調用回調方法的程式碼實際上終止調用所有的在鏈表中由委派對象封裝的回調方法。也就是說,對於被迭代的每一個項目,都會調用FeedbackToConsole,接著馬上調用FeedbackToMsgBox。在後續文章中我將詳細討論委派鏈的處理機制。
有一點很重要,那就是在這個例子中每件事情都是型別安全的,例如,當構造Feedback委派對象時,編譯器保證App的FeedbackToConsole和FeedbackToMsgBox方法都具備確切的原型,像由Feedback委派定義的一樣。既兩個方法必須有三個參數(Object,Int32和Int32),並且兩個方法必須有相同的傳回型別(void)。如果方法原型不匹配,則編譯器將發出下面的出錯資訊:“error CS0123:The signature of method 'App.FeedbackToMsgBox()' does not match this delegate
type。”——意思是App.FeedbackToMsgBox()方法的簽名與委派的類型不符。
調用執行個體方法
前面我們討論了如何使用委派調用靜態方法。但是委派還能被用於調用特定對象的執行個體方法。在調用執行個體方法時,委派需要知道這個它要用方法操作的對象的執行個體。
為了理解執行個體方法的回調機制,讓我們回頭看看前面代碼中的InstanceCallbacks方法。這段代碼與靜態方法的情形極其相似。注意在Set對象被建立之後,App對象被建立。這個App對象僅僅是建立而已,處於樣本目的沒有其它內容。當新的Feedback委派對象被建立的時候,它的構造齊備傳到appobj.FeedbackToFile。這將導致這個委派封裝對FeedbackToFile方法的引用,FeedbackToFile是個執行個體方法(非靜態)。當這個執行個體方法被調用時,由appobj引用的對象被操作(作為隱藏傳遞參數)。FeedbackToFile方法的作用有點像FeedbackToConsole 和 FeedbackToMsgBox,不同的是它開啟一個檔案並將處理的項目串添加到檔案尾。

揭開委派的神秘面紗
從表面上看,委派好像很容易使用:用C#委派關鍵字定義,用類似new操作符的方式構造它們的執行個體, 用類似方法調用的文法調用回調方法(不同的是不使用方法名,而是使用指代委派對象的變數)。
然而,委派的實際運行機制要比前述例子中所描述的過程要複雜的多。編譯器和通用語言執行平台(CLR)在幕後所做的許多處理隱藏了這些複雜性,在這一部分中,我們將集中精力來討論編譯器和CLR是如何協同工作實現委派機制的。這些知識將極大地豐富你對委派的理解並且這些知識將告訴你如何有效地使用它們。我們還將涉及到一些在編程中能用到的委派的附加特性。
我們還是從下面這行代碼開始:
public delegate void Feedback(
Object value, Int32 item, Int32 numItems);

當編譯器看到之一行代碼時,它會產生一個完整的類定義,這個定義的代碼會像下面這個樣子:
//
public class Feedback : System.MulticastDelegate {
// 構造器
public Feedback(Object target, Int32 methodPtr);

// 方法與原始碼描述的原型相同
public void virtual Invoke(
Object value, Int32 item, Int32 numItems);

// 方法允許被非同步回調,後繼文章將討論這些方法
public virtual IAsyncResult BeginInvoke(
Object value, Int32 item, Int32 numItems,
AsyncCallback callback, Object object);
public virtual void EndInvoke(IAsyncResult result);
}
//

事實上,通過使用ILDasm.exe程式檢查結果模組(如圖三),你能發現編譯器確實自動產生了這個類。

圖三 檢查編譯器產生的類
在這個例子中,編譯器已經定義了一個叫Feedback的從System.MulticastDelegate類型派生的類,它是在架構類庫(Framework Class Library)中定義的。要知道,所有委派類型都是從MulticastDelegate派生出來的。在這個例子中,Feedback類是公用(public)類型的,因為在原始碼中它的類型被定義為public。如果用私人(private)或者受保護的(protected)類型定義,則由編譯器產生的Feedback類也將是私人或者受保護的類型。你應該注意到委派類可能會在某個類中定義(如例子中Feedback就是在Set類中定義的);委派也可能在被定義為全域型。從本質上說,可以將委派看成是類,可以在定義類的任何地方定義委派。
因為所有的委派都派生於MulticastDelegate,它們繼承了MulticastDelegate的域,屬性和方法。在所有這些成員中,你要特別注意三個私人(private)域:
用於委派類型的私人域:
域 類型 描述
_target System.Object 指回呼函數被調用時應該操作的對象。用於執行個體方法回調
_methodPtr System.Int32 內部整型,CLR用它來標示被回調的方法
_prev System.MulticastDelegate 指另一個委派對象,通常為null

所有的委派都有代兩個參數的構造器:一個參數是對象引用,一個是指代回調方法的整型。但是,如果你檢查原始碼,就會發現明白諸如App.FeedbackToConsole 或 appobj.FeedbackToFile的傳遞使用值進行的。你的敏感會告訴你這個代碼不能編譯!
然而,編譯器知道某個委派被建立,同時編譯器解析原始碼以決定引用哪個對象和方法。對象引用被傳遞為目標參數,並且用某個特定的Int32值(從某個MethodDef或MethodRef中繼資料符號獲得)標示的方法被傳遞為methodPtr參數。對於靜態方法,null被傳遞為目標參數。在構造器內部,這兩個參數被儲存在它們對應的私人(private)域中。
另外,構造器將這個域置為null。這個域被用來建立一個MulticastDelegate對象鏈表。現在我們暫時忽略_prev域,在後續文章中將會詳細討論有關它的內容。
每一個委派對象實際上就是一個方法封裝器,當方法被調用時,受作用的對象被操作。MulticastDelegate類定義兩個唯讀公用執行個體屬性:Target和Method。給定一個委派對象引用,你就可以查詢到它的這些屬性。如果方法被回調,Target屬性返回一個對將要操作的對象的引用。如果方法是靜態,則Target返回null。Method屬性返回標示回調方法的System.Reflection.MethodInfo對象。
你可以用幾種方式使用這些資訊。一種方式是檢查是否某個委派對象引用特定類型的執行個體方法:
//
Boolean DelegateRefersToInstanceMethodOfType(
MulticastDelegate d, Type type) {

return((d.Target != null) && d.Target.GetType == type);
}
//

你還應該編寫代碼檢查是否回調方法由專門的名字(如FeedbackToMsgBox):
//
Boolean DelegateRefersToMethodOfName(
MulticastDelegate d, String methodName) {

return(d.Method.Name == methodName);
}
//

現在你知道了如何構造委派對象,下面讓我們來談談回調方法是如何被調用的。為方便起見,我們還是使用
Set類中的ProcessItems:
//
public void ProcessItems(Feedback feedback) {
for (Int32 item = 1; item <= items.Length; item++) {
if (feedback != null) {
// 如果指定任何回調,則調用它們
feedback(items[item], item, items.Length);
}
}
}
//

注釋行下面的那一行代碼就是調用回調方法。仔細看看代碼,它調用feedback函數並傳遞三個參數。但是feedback是不存在的。再一次指出,編譯器知道feedback是個引用某個委派對象的變數,並且編譯器會產生實際的代碼來調用委派對象的Invoke方法。換句話說,編譯器看到下面這行代碼後:
feedback(items[item], item, items.Length);

編譯器產生的結果與下面這行原始碼產生的結果一樣:
feedback.Invoke(items[item], item, items.Length);

事實上,通過使用ILDasm.exe程式檢查ProcessItems代碼結果(如圖五),你能發現這一點。

圖五 分解後的 Set類的 ProcessItems
圖五顯示了用於Set類型中ProcessItems方法的微軟中介語言。其中紅色的箭頭指向的指令調用Set.Feedback的Invoke方法。如果你修改原始碼來顯式調用Invoke方法,C#編譯器報錯,出錯資訊為:“error CS1533: Invoke cannot be called directly on a delegate”——意思是Invoke不能針對某個委派被直接調用。C#不允許你顯式調用Invoke(但是,但別的編譯器可以)。
你會想起當編譯器定義Feedback類的時候,它也定義了Invoke方法。當Invoke被調用時,它使用私人的_target和_methodPtr域來為特定對象調用希望的方法。注意Invoke方法的簽名與委派的簽名要完全符合。也就是說,Feedback委派帶三個參數並返回void,那麼Invoke方法也必須帶三個相同的參數並返回void。



聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.