C#中的委託(爲什麼C#調用dll的回調函數用委託)

簡介

       委託是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)

{

       return(x+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);

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

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

                            break;

                     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 函數中,有些函數要求正確地運行託管代碼中的回調函數。關於回調函數只是使用到委託,在此不加過多說明

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章