天天看點

C#中的委托

< DOCTYPE html PUBLIC -WCDTD XHTML StrictEN httpwwwworgTRxhtmlDTDxhtml-strictdtd>

關鍵字     C#,.NET,delegate,函數指針,event

簡介

       委托是C#中的一種引用類型,類似于C/C++中的函數指針。與函數指針不同的是,委托是面向對象、類型安全的,而且委托可以引用靜态方法和執行個體方法,而函數指針隻能引用靜态函數。委托主要用于 .NET Framework 中的事件處理程式和回調函數。

       一個委托可以看作一個特殊的類,因而它的定義可以像正常類一樣放在同樣的位置。與其他類一樣,委托必須先定義以後,再執行個體化。與類不同的是,執行個體化的委托沒有與之相應的術語(類的執行個體化稱作對象),作為區分我們将執行個體化的委托稱為委托執行個體。

函數指針

       一個函數在編譯時被配置設定給一個入口位址,這個入口位址就稱為函數的指針,正如同指針是一個變量的位址一樣。

函數指針的用途很多,最常用的用途之一是把指針作為參數傳遞到其他函數。我們可以參考下面的例子進一步了解函數指針作為參數的情況:

# include<stdio.h>

int max(int x,int y)

{

       return (x>y?x:y);

}

int min(int x,int y)

       return(x<y?x:y);

int sub(int x, int y)

       return(x+y);

int minus(int x,int y)

{    

       return(x-y);

void test(int (*p)(int,int),int (*q)(int,int),int a,int b)

       int Int1,Int2;

       Int1=(*p)(a,b);

       Int2=(*q)(a,b);

       printf("%d,\t%d\n",Int1,Int2);

void main()

       test(max,min,10,3);

       test(sub,minus,10,3);

客觀的講,使用函數指針作為其參數的函數如果直接調用函數或是直接把調用的函數的函數體放在這個主函數中也可以實作其功能。那麼為什麼還要使用函數指針呢?我們仔細看一下上面的main()函數就可以發現,main()函數兩次調用了test函數,前一次求出最大最小值,後一次求出兩數的和與差。如果我們test函數不用函數指針,而是采用直接在test函數中調用函數的方法,使用一個test函數還能完成這個功能嗎?顯然不行,我們必須寫兩個這樣的test函數供main()函數調用,雖然大多數代碼還是一樣的,僅僅是調用的函數名不一樣。上面僅僅是一個簡單的例子,實際生活中也許main()函數會頻繁的調用test(),而每次的差别僅僅是完成的功能不一樣,也許第一次調用會要求求出兩數的和與差,而下一次會要求求出最大值以及兩數之和,第三次呢,也許是最小值和最大值,……,如果不用函數指針,我們需要寫多少個這樣的test()函數?顯然,函數指針為我們的程式設計提供了靈活性。

另外,有些地方必須使用到函數指針才能完成給定的任務,特别是異步操作的回調和其他需要匿名回調的結構。另外,像線程的執行,事件的處理,如果缺少了函數指針的支援也是很難完成的。

類型安全

從上面的介紹可以看出,函數指針的提出還是有其必要的,上面的介紹也同時說明了委托存在的必要性。那麼為什麼C#中不直接用函數指針,而是要使用委托呢?這就涉及到另外一個問題:C#是類型安全的語言。何謂類型安全?這裡的類型安全特指記憶體類型安全,即類型安全代碼隻通路被授權可以通路的記憶體位置。如果代碼以任意偏移量通路記憶體,該偏移量超出了屬于該對象的公開字段的記憶體範圍,則它就不是類型安全的代碼。顯然指針不屬于類型安全代碼,這也是為什麼C#使用指針時必須申明unsafe的緣故。

那麼類型不安全代碼可能會帶來什麼不良的後果呢?相信對于安全技術感興趣的朋友一定十分熟悉緩沖區溢出問題,通過緩沖區溢出攻擊者可以運作非法的程式獲得一定的權限進而攻擊系統或是直接運作惡意代碼危害系統,在UNIX下這是一個十分普遍的問題。那麼緩沖區溢出又和函數指針有什麼關系呢?事實上,攻擊者就是通過緩沖區溢出改變傳回位址的值到惡意代碼位址來執行惡意代碼的。我們可以看看下面的代碼:

void copy()

            {

                 char buffer[128];

                 ........

                     strcpy (buffer,getenv("HOME"));//HOME為UNIX系統中的HOME環境變量

            }

上面的代碼中如果HOME環境變量的字元數大于128,就會産生緩沖區溢出,假如這個緩沖區之前有另一個函數的傳回位址,那麼這一是位址就有可能覆寫,而覆寫這一位址的字元有可能就是惡意代碼的位址,攻擊者就有可能攻擊成功了!

上面的例子僅僅是指針問題中的一種,除此以外,還可能由于錯誤的管理位址,将資料寫入錯誤位址,造成程式的崩潰;還可能由于對指針不恰當的指派操作産生懸浮指針;還可能産生記憶體越界,記憶體洩漏等等問題。

由此可見,指針不是類型安全的,函數指針當然也不例外,是以C#裡面沒有使用函數指針,而且不建議使用指針變量。

委托

前面的說明充分證明了委托存在的必要性,那麼我們再談談為什麼委托是類型安全的。C#中的委托和指針不一樣,指針不通過MSIL而是直接和記憶體打交道,這也是指針不安全的原因所在,當然也是采用指針能夠提高程式運作速度的緣故;委托不與記憶體打交道,而是把這一工作交給CLR去完成。CLR無法阻止将不安全的代碼調用到本機(非托管)代碼中或執行惡意操作。然而當代碼是類型安全時,CLR的安全性強制機制確定代碼不會通路本機代碼,除非它有通路本機代碼的權限。

委托派生于基類System.Delegate,不過委托的定義和正常類的定義方法不太一樣。委托的定義通過關鍵字delegate來定義:

public delegate int myDelegate(int x,int y);

上面的代碼定義了一個新委托,它可以封裝任何傳回為int,帶有兩個int類型參數的方法。任何一個方法無論是執行個體方法還是靜态方法,隻要他們的簽名(參數類型在一個方法中的順序)和定義的委托是一樣的,都可以把他們封裝到委托中去。這種簽名方法正是保證委托是類型安全的手段之一。

産生委托執行個體和産生類執行個體(對象)差不多,假如我們有如下的方法:

public int sub(int x,int y)

我們就可以使用如下的代碼得到一個委托執行個體:

myDelegate calculatin=new myDelegate(sub);

接下來我們就可以直接使用calculation調用sub方法了:

calculation(10,3);

下面我們将用委托重寫上面的一個程式來看一下在C#中如何通過委托實作由函數指針實作的功能:

using System;

class MathClass

       public static int max(int a,int b)

       {

              return(a>b?a:b);

       }

       public static int min(int a,int b)

              return(a<b?a:b);

       public static int sub(int a,int b)

              return (a+b);

       public static int minus(int a,int b)

              return (a-b);

class Handler

       private delegate int Calculation(int a, int b);

       private static Calculation[] myCalculation=new Calculation[2];

       public static void EventHandler(int i,int a,int b)

              switch (i)

              {

                     case 1:

                            myCalculation[0]=new Calculation(MathClass.max);

                            myCalculation[1]=new Calculation(MathClass.min);

                            Console.WriteLine(myCalculation[0](a,b));

                            Console.WriteLine(myCalculation[1](a,b));

                            break;

                     case 2:

                            myCalculation[0]=new Calculation(MathClass.sub);

                            myCalculation[1]=new Calculation(MathClass.minus);

                     default:

                            return;

              }

class Test

       static void Main()

              Handler.EventHandler(1,10,3);

              Handler.EventHandler(2,10,3);

我們還可以聲明一個委托數組,就像聲明一個對象數組一樣,上面的例子中就使用到了委托數組;一個委托還可以封裝多個方法(多路廣播委托,經常與事件處理程式結合使用),隻要這些方法的簽名是正确的。多路廣播委托的傳回值一般為void,這是因為一個委托隻能有一個傳回值,如果一個傳回值不為void的委托封裝了多個方法時,隻能得到最後封裝的方法的傳回值,這可能和使用者初衷不一緻,同時也會給管理帶來不友善。如果你想通過委托傳回多個值,最好是使用委托數組,讓每個委托封裝一個方法,各自傳回一個值。

事件

在C#中,委托的最基本的一個用處就是用于事件處理。事件是對象發送的消息,以發信号通知操作的發生,通俗一點講,事件就是程式中産生了一件需要處理的信号。

事件的定義用關鍵字event聲明,不過聲明事件之前必須存在一個多路廣播委托:

public delegate void Calculate(int x,int y);//傳回值為void的委托自動成為多路廣播委托;

public event  calculate OnCalculate;

從上節的委托執行個體和上面的事件的聲明可以看出,事件的聲明僅僅是比委托執行個體的聲明多了個關鍵字event,事實上事件可以看作是一個為事件處理過程定制的多路廣播委托。是以,定義了事件後,我們就可以通過向事件中操作符+=添加方法實作事件的預定或者是通過-=取消一個事件,這些都與委托執行個體的處理是相同的。與委托執行個體不同的是,操作符=對于事件是無效的,即

OnCalculate=new calculate(sub) ;//無效

隻是因為上面的語句會删除由OnCalculate封裝的所有其他方法,指封裝了由此語句指定的唯一方法,而且一個預定可以删除其他所有方法,這會導緻混亂。

回調函數

回調函數是在托管應用程式中可幫助非托管 DLL 函數完成任務的代碼。對回調函數的調用将從托管應用程式中,通過一個 DLL 函數,間接地傳遞給托管實作。在用平台調用調用的多種 DLL 函數中,有些函數要求正确地運作托管代碼中的回調函數。關于回調函數隻是使用到委托,在此不加過多說明,具體實作可參考下圖:

本文轉自 netcorner 部落格園部落格,原文連結http://www.cnblogs.com/netcorner/archive/2006/11/08/2912546.html:,如需轉載請自行聯系原作者