C# Delegate 簡介

來源:互聯網
上載者:User
作者:Stanley B. Lippman  來自 方舟 Blog
[譯序:這是一篇古老的文章。但毫無疑問,Lippman對delegate的闡述是精闢的。]

如果你想拿 C# 與其它“C家族”的語言做比較,C# 正有個不同尋常的特性,其在 C++ 或者 Java 裡沒有真正意義上的對應之物。

--------------------------------------------------------------------------------

C# 是一個頗具爭議的新興語言,由 Microsoft 開發創造,以作為其 Visual Studio.NET 的基石,目前正處於第一個 Beta 版的發布階段。C# 結合了源自 C++ 和 Java 的許多特性。Java 社群對 C# 主要的批評在於,其聲稱 C# 只是一個蹩腳的 Java 複製版本 ——與其說它是語言創新的成果,倒不如說是一樁訴訟的結果。而在 C++ 社群裡,主要的批評(也同時針對 Java)是,C# 只不過是另一個泛吹濫捧的私人語言(yet another over-hyped proprietary language)。

本文意在展示一種 C# 的語言特性,而在 C++ 或 Java 中都沒有直接支援類似的特性。這就是 C# 的 delegate 型別,其運作近似於一種指向成員函數的指標。我認為,C# delegate 型別是經過深思熟慮的創新型語言特性,C++ 程式員(無論其對 C# 或者 Microsoft 有何想法)應該會對這個特性產生特殊的興趣。

為了激發討論,我將圍繞一個 testHarness class 的設計來進行闡述。這個 testHarness class 能夠讓任何類別對 static 或 non-static 的 class methods 進行註冊,以便後續予以執行。Delegate 型別正是實現 testHarness class 的核心。

C# 的 Delegate Type
Delegate 是一種函數指標,但與普通的函數指標相比,區別主要有三:

1) 一個 delegate object 一次可以搭載多個方法(methods)[譯註1],而不是一次一個。當我們喚起一個搭載了多個方法(methods)的 delegate,所有方法以其“被搭載到 delegate object 的順序”被依次喚起——稍候我們就來看看如何這樣做。

2) 一個 delegate object 所搭載的方法(methods)並不需要屬於同一個類別。一個 delegate object 所搭載的所有方法(methods)必須具有相同的原型和形式。然而,這些方法(methods)可以即有 static 也有 non-static,可以由一個或多個不同類別的成員組成。

3) 一個 delegate type 的聲明在本質上是建立了一個新的 subtype instance,該 subtype 派生自 .NET library framework 的 abstract base classes Delegate 或 MulticastDelegate,它們提供一組 public methods 用以詢訪 delegate object 或其搭載的方法(methods)

聲明 Delegate Type
一個 delegate type 的聲明一般由四部分組成:(a) 存取層級;(b) 關鍵字 delegate;(c)返回型別,以及該 delegate type 所搭載之方法的聲明形式(signature);(d) delegate type 的名稱,被放置於返回型別和方法的聲明形式(signature)之間。例如,下面聲明了一個 public delegate type Action,用來搭載“沒有參數並具有 void 返回型別”的方法:

public delegate void Action();

一眼看去,這與函數定義驚人的相似;唯一的區別就是多了 delegate 關鍵字。增加該關鍵字的目的就在於:要通過關鍵字(keyword)——而非字元(token)——使普通的成員函數與其它形似的文法形式區別開來。這樣就有了 virtual,static, 以及 delegate 用來區分各種函數和形似函數的文法形式。

如果一個 delegate type 一次只搭載單獨一個方法(method),那它就可以搭載任意返回型別及形式的成員函數。然而,如果一個 delegate type 要同時搭載多個方法(methods),那麼返回型別就必須是 void[譯註2]。 例如,Action 就可以用來搭載一個或者多個方法(method)。在 testHarness class 實現中,我們就將使用上述的 Action 聲明。

定義 Delegate Handle
在 C# 中我們無法聲明全域對象;每個對象定義必須是下述三種之一:局部對象;或者型別的對象成員;或者函數參數列表中的參數。現在我只向你展示 delegate type 的聲明。之後我們再來看如何將其聲明為類別中的成員。

C# 中的 delegate type 與 class, interface, 以及 array types 一樣,屬於 reference type。每個 reference type 被分為兩部分:

一個具名的 控制代碼(named handle),由我們直接操縱;以及
一個該控制代碼所屬型別的不具名對象(unamed object),由我們通過控制代碼間接進行操縱。必須經由 new 顯式的建立該對象。
定義 reference type 是一個“兩步走”的過程。當我們寫:

Action theAction;

的時候,theAction 代表“delegate type Action 之對象”的一個 handle(控制代碼),其本身並非 delegate object。預設情況下,它被設為 null。如果我們試圖在對其賦值(譯註:assigned,即與相應型別的對象做attachment)之前就使用它,會發生編譯期錯誤。例如,語句:

theAction();

會喚起 theAction 所搭載的方法(method(s))。然而,除非它在定義之後、使用之前被無條件的賦值(譯註:assigned,即與相應型別的對象做attachment),否則該語句會引發編譯期錯誤並印出相關資訊。

為 Delegate Object 分配空間
在這一節中,為了以最小限度的涉及面繼續進行闡述,我們需要訪問一個靜態方法(static method)和一個非靜態方法(non-static method),就此我採用了一個 Announce class。該類別的 announceDate 靜態方法(static method)以 long form 的形式(使用完整單字的冗長形式)列印當前的日期到標準輸出裝置:

Monday, February 26, 2001

非靜態方法(non-static method) announceTime 以 short form 的形式(較簡短的表示形式)列印目前時間到標準輸出裝置:

00:58

前兩個數字代表小時,從午夜零時開始計算,後兩個數字代表分鐘。Announce class 使用了由 .NET class framework 提供的 DateTime class。Announce 類別的定義如下所示。

public class Announce
{
   public static void announceDate()
   {
      DateTime dt = DateTime.Now;
      Console.WriteLine( "Today''''s date is {0}",
                         dt.ToLongDateString() );
   }
   public void announceTime()
   {
      DateTime dt = DateTime.Now;
      Console.WriteLine( "The current time now is {0}",
                         dt.ToShortTimeString() );
   }
}

要讓 theAction 搭載上述方法,我們必須使用 new 運算式建立一個 Action delegate type(譯註:即建立一個該類別的對象)。要搭載靜態方法,則傳入建構函式的引數由三部分組成:該方法所屬類別的名稱;方法的名稱;分隔兩個名稱用的 dot operator(.):

theAction = new Action( Announce.announceDate );

要搭載非靜態方法,則傳入建構函式的引數也由三部分組成:該方法所屬的類別對象名稱;方法的名稱;分隔兩個名稱用的 dot operator(.):

Announce an = new Announce();
theAction   = new Action( an.announceTime );

可以注意到, theAction 被直接賦值,事先沒有做任何檢查(比如,檢查它是否已經指代一個堆中的對象,如果是,則先刪除該對象)。在 C# 中,存在於 managed heap(受託管的堆)中的對象由運行期環境對其施以垃圾收集動作(garbage collected)。我們不需要顯式的刪除那些經由 new 運算式分配的對象。

在程式的 managed heap(受託管的堆)中,new 運算式既可以為獨個對象做分配

HelloUser myProg = new HelloUser();

也可以為數組對象做分配

string [] messages = new string[ 4 ];

分配語句的形式為:型別的名稱,後跟關鍵字 new,後跟一對圓括弧(表示單個對象)或者方括弧(表示數組對象)[1]。(在 C# 語言設計中的一個普遍特徵就是,堅持使用單一明晰的形式來區別不同的功用。)

一個快速的概覽:Garbage Collection(垃圾收集)
如下述數組對象所示,當我們在 managed heap(受託管的堆)中為 reference type 分配了空間:

int [] fib = new int[6]{ 1,1,2,3,5,8 };

對象自動的維護“指向它的控制代碼(handles)”之數目。在這個例子中,被 fib 所指向的數組對象有一個關聯的引用計數器被初始化為1。如果我們現在初始化另一個控制代碼,使其指向 fib 所指代的數組對象:

int [] notfib = fib;

這次初始化導致了對 fib 所指代數組對象的一次 shallow copy(淺層拷貝)。這就是說,notfib 現在也指向 fib 所指向的數組對象。該數組對象所關聯的引用計數變成了2。

如果我們經由 notfib 修改了數組中某個元素,比如

notfib [ 0 ] = 0;

這個改變對於 fib 也是可見的。如果這種對同一個對象的多重訪問方式並非所需,我們就需要編寫代碼,做一個 deep copy(深層拷貝)。例如,

// 分配另一個數組對象
notfib = new int [6];
// 從 notfib 的第0個元素開始,
// 依次將 fib 中的元素拷貝到 notfib 中去。
// 見注釋 [2]
fib.CopyTo( notfib, 0 );

notfib 現在並不指代 fib 所指代的那個對象了。先前被它們兩個同時指向的那個對象將其關聯的引用計數減去1。notfib 所指代對象的初始引用計數為1。如果我們現在也將 fib 重新賦值為一個新的數組對象——例如,一個包含了Fibonacci數列前12個數值的數組:

fib = new int[12]{ 1,1,2,3,5,8,13,21,34,55,89,144 };

對於之前被 fib 所指代的那個數組對象,其現在的引用計數變成了0。在 managed heap(受託管的堆)中,當垃圾收集器(garbage collector)處於活動狀態時,引用計數為0的對象被其作上刪除標記。

定義 Class Properties
現在讓我們將 delegate object 聲明為 testHarness class 的一個私人靜態(private static)成員。例如 [3],

public class testHarness
{
   public delegate void    Action();
   static private  Action  theAction;
   // ...
}

下一步我們要為這個 delegate 成員提供讀寫訪問機制。在 C# 中,我們不要提供顯式的內聯方法(inline methods)用來讀寫非公有的資料成員。取而代之,我們為具名的屬性(named property)提供 get 和 set 訪問符(accessors)。下面是個簡單的 delegate property。我們不妨將其稱為 Tester:

public class testHarness
{
   static public Action Tester
   {
      get{ return theAction; }
      set{ Action = value; }
   }
   // ...
}

Property(屬性)既可以封裝待用資料成員,也可以封裝非待用資料成員。Tester 就是 delegate type Action 的一個 static property(靜態屬性)。(可以注意到。我們將 accessor 定義為一個代碼區塊。編譯器內部由此產生 inline method。)

get 必須以 property(屬性)的型別作為返回型別。在這個例子中,其直接返回所封裝的對象。如果採用“緩式分配(lazy allocation)”,get 可以在初次被喚起的時候建構並存放好對象,以便後用。

類似的,如果我們希望 property(屬性)能夠支援寫入型訪問,我們就提供 set accessor。set 中的 value 是一個條件型關鍵字(conditional-keyword)。也就是說,value 僅在 set property 中具有預定義的含義(譯註:也就是說,value 僅在 set 程式碼片段中被看作一個關鍵字):其總是代表“該 property(屬性)之型別”的對象。在我們的例子中,value 是 Action 型別的對象。在運行期間,其被綁定到賦值運算式的右側。在下面的例子中,

Announce an = new Announce();
testHarnes.Tester =
    new testHarness.Action
    ( an.announceTime );

set 以內聯(inline)的方式被展開到 Tester 出現的地方。value 對象被設定為由 new 運算式返回的對象。

喚起 Delegate Object
如之前所見,要喚起由 delegate 所搭載的方法,我們對 delegate 施加 call operator(圓括弧對):

testHarness.Tester();

這一句喚起了Tester property 的 get accessor;get accessor返回 theAction delegate handle。如果 theAction 在此刻並未指向一個 delegate object,那麼就會有異常被拋出。從類別外部實行喚起動作的規範做法(delegate-test-and-execute,先實現代理,再測試,最後執行之)如下所示:

if ( testHarness.Tester != null )
   testHarness.Tester();

對於 testHarness class,我們的方法只簡單的封裝這樣的測試:

static public void run()
{
   if ( theAction != null )
      theAction();
}

關聯多個 Delegate Objects
要讓一個 delegate 搭載多個方法,我們主要使用 += operator 和 -= operator。例如,設想我們定義了一個 testHashtable class。在建構函式中,我們把各個關聯的測試加入到 testHarness 中:

public class testHashtable
{
   public void test0();
   public void test1();
   testHashtable()
   {
      testHarness.Tester += new testHarness.Action( test0 );
      testHarness.Tester += new testHarness.Action( test1 );
   }
   // ...
}

同樣,如果我們定義一個 testArrayList class,我們也在 default constructor 中加入關聯的測試。可以注意到,這些方法是靜態。

public class testArrayList
{
   static public void testCapacity();
   static public void testSearch();
   static public void testSort();
   testArrayList()
   {
      testHarness.Tester += new
         testHarness.Action(testCapacity);
      testHarness.Tester += new testHarness.Action(testSearch);
      testHarness.Tester += new testHarness.Action(testSort);
   }
   // ...
}

當 testHarness.run 方法被喚起時,通常我們並不知道 testHashtable 和 testArrayList 中哪一個的方法先被喚起;這取決於它們建構函式被喚起的順序。但我們可以知道的是,對於每個類別,其方法被喚起的順序就是方法被加入 delegate 的順序。

Delegate Objects 與 Garbage Collection(垃圾收集)
考察下列局部範圍中的程式碼片段:

{
   Announce an = new Announce();
   testHarness.Tester +=
      new testHarness.Action
      ( an.announceTime );
}

當我們將一個非靜態方法加入到 delegate object 中之後,該方法的地址,以及“用來喚起該方法,指向類別對象的控制代碼(handle)”都被儲存起來。這導致該類別對象所關聯的引用計數自動增加。

an 經由 new 運算式初始化之後,managed heap(受託管的堆)中的對象所關聯的引用計數被初始化為1。當 an 被傳給 delegate object 的建構函式之後,Announce 對象的引用計數增加到2。走出局部範圍之後,an 的生存期結束,該引用計數減回到1——delegate object還佔用了一個。

好訊息是,如果有一個 delegate 引用了某對象的一個方法,那麼可以保證該對象會直到“delegate object 不再引用該方法”的時候才會被施以垃圾收集處理[4]。我們不用擔心對象會在自己眼皮底下被貿然清理掉了。壞訊息是,該對象將持續存在(譯註:這可能是不必要的),直到 delegate object 不再引用其方法為止。可以使用 -= operator 從 delegate object 中移除該方法。例如下面修正版本的代碼;在局部範圍中,announceTime 先被設定、執行,然後又從 delegate object 中被移除。

{
   Announce an = new Announce();
   Action act  = new testHarness.Action( an.announceTime );
   testHarness.Tester += act;
   testHarness.run();
   testHarness.Tester -= act;
}

我們對於設計 testHashtable class 的初始想法是,實現一個解構函式用以移除在建構函式中加入的測試用方法。然而,C# 中的解構函式調用機制與 C++ 中的卻不大相同[5]。C# 的解構函式既不會因為物件存留期結束而跟著被喚起,也不會因為釋放了對象最後一個引用控制代碼( reference handle)而被直接喚起。事實上,解構函式僅在垃圾收集器作垃圾收集時才被調用,而施行垃圾收集的時機一般是無法預料的,甚至可以根本就沒施行垃圾收集。

C# 規定,資源去配動作被放進一個稱為 Dispose 的方法中完成,使用者可以直接調用該方法:

public void Dispose ()
{
   testHarness.Tester -= new testHarness.Action( test0 );
   testHarness.Tester -= new testHarness.Action( test1 );
}

如果某類別定義了一個解構函式,其通常都會喚起 Dispose。

訪問底層的類別介面
讓我們再回頭看看先前的代碼:

{
   Announce an = new Announce();
   Action act  =
       new testHarness.Action
       ( an.announceTime );
   testHarness.Tester += act;
   testHarness.run();
   testHarness.Tester -= act;
}

另一種實現方案是,先檢查 Tester 當前是否已經搭載了其它方法,如果是,則儲存當前的委託列表(delegation list),將 Tester 重設為 act,然後調用 run,最後將 Tester 恢複為原來的狀態。

我們可以利用底層的 Delegate 類別介面來獲知 delegate 實際搭載的方法數目。例如,

if ( testHarness.Tester != null &&
     testHarnest.GetInvocationList().Length != 0 )
   {
      Action oldAct = testHarness.Tester;
      testHarness.Tester = act;
      testHarness.run();
      testHarness.Tester = oldAct;
   }   
else { ... }

GetInvocationList 返回 Delegate class objects 數組,數組的每個元素即代表該 delegate 當前搭載的一個方法。Length 是底層 Array class 的一個 property(屬性)。Array class 實現了 C# 內建數組型別的語義[6]。

經由 Delegate class 的 Method property,我們可以擷取被搭載方法的全部運行期資訊。如果方法是非靜態,那麼經由 Delegate class 的 Target property,我們更可以擷取調用該方法之對象(譯註:即該方法所屬類別的那個對象)的全部運行期資訊。在下面例子中,Delegate 的 methods(方法) 和 properties(屬性)用紅色表示:

If (testHarness.Tester != null )
{
   Delegate [] methods = test.Tester.GetInvocationList();
   foreach ( Delegate d in methods )
   {
      MethodInfo theFunction = d.Method;
      Type       theTarget   = d.Target.GetType();
   // 好的:現在我們可以知道 delegate 所搭載方法的全部資訊
   }
}

總結
希望本文能夠引起你對 C# delegate type 的興趣。我認為 delegate type 為 C# 提供了一種創新性的“pointer to class method(類別方法之指標)”機制。或許本文還引起了你對 C# 語言以及 .NET class framework 的興趣。

A good starting page for technical resources is <http://www.microsoft.com/net/>. An informative news group with Microsoft developer input dealing with both .NET and C# is <http://discuss.develop.com/dotnet.html>. Of course, questions or comments on C# or this article can be addressed to me at stanleyl@you-niversity.com. Finally, C# is currently in the process of standardization. On October 31, 2000, Hewlett-Packard, Intel, and Microsoft jointly submitted a proposed C# draft standard to ECMA, an international standards body (ECMA TC39/TG2). The current draft standard and other documentation can be found at <http://www.ecma.ch>.

致謝
I would like to thank Josee Lajoie and Marc Briand for their thoughtful review of an earlier draft of this article. Their feedback has made this a significantly better article. I would also like to thank Caro Segal, Shimon Cohen, and Gabi Bayer of you-niversity.com for providing a safety.NET.

注釋
[1] 對於 C++ 程式員來說,有兩點值得一題:(a) 需要在對象的型別名稱之後放一對圓括弧作為 default constructor,以及(b) 用於數組下標的方括弧要放在型別與數組名稱之間。

[2] C# 中內建的數組是一種由 .NET class library 提供的 Array class 之對象。Array class 的靜態方法和非靜態方法都可以被 C# 內建數組對象使用。CopyTo 是 Array 的一個非靜態方法。

[3] 與 Java 一樣,C# 中的成員聲明包括其存取層級。預設的存取層級是 private。

[4] 類似的,C++ 標準要求,被引用的臨時對象必須直到引用的生存期結束時才能夠被銷毀。

[5] 在內部實現中,解構函式甚至都不曾存在過。一個類別的解構函式會被轉換成 virtual Finalize 方法。

[6] 在 C# 中,一個條件判別式的結果必須得到 Boolean 型別。對 Length 值的直接判別,如if(testHarness.Length),並不是合法的條件判斷。整型值無法被隱式的轉換為 Boolean 值。

Stanley B. Lippman is IT Program Chair with you-niversity.com, an interactive e-learning provider of technical courses on Patterns, C++, C#, Java, XML, ASP, and the .NET platform. Previously, Stan worked for over five years in Feature Animation both at the Disney and DreamWorks Animation Studios. He was the software Technical Director on the Firebird segment of Fantasia 2000. Prior to that, Stan worked for over a decade at Bell Laboratories. Stan is the author of C++ Primer, Essential C++, and Inside the C++ Object Model. He is currently at work on C# Primer for the DevelopMentor Book Series for Addison-Wesley. He may be reached at stanleyl@you-niversity.com.

譯註
[譯註1]  在C#中,所謂“method(方法)”,其實就是指我們平常所理解的成員函數,其字面意義與“function(函數)”非常接近。

[譯註2]  作者是就前述的那個 delegate type Action 聲明而有此言。就一般而言,只要多個方法(methods)的返回型別相同並且參數也相同,就可以被同一個 delegate type 搭載。

推薦: 編寫更快的Managed 程式碼:瞭解開銷情況
http://www.microsoft.com/china/msdn/archives/library/dndotnet/html/fastmanagedcode.asp

310674 - HOW TO: 添加對託管的 Visual C++ 項目的引用
http://support.microsoft.com/default.aspx?scid=kb;zh-cn;310674

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.