C# Delegate 引介 (轉)

amyz發表於2007-11-16
C# Delegate 引介 (轉)[@more@]

C# Delegate 引介

Stanley B. Lippman

--&gt

[譯序:這是一篇古老的文章。但毫無疑問,Lippman對delegate的闡述是精闢的。]

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


C# 是一個頗具爭議的新興語言,由 開發創造,以作為其 的基石,目前正處於第一個 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 一次可以搭載多個方法(methods),而不是一次一個。當我們喚起一個搭載了多個方法(methods)的 delegate,所有方法以其“被搭載到 delegate object 的順序”被依次喚起——稍候我們就來看看如何這樣做。

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

3) 一個 delegate type 的宣告在本質上是建立了一個新的 subtype instance,該 subtype 派生自 library 的 abstract base classes DelegateMulticastDelegate,它們提供一組 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 關鍵字。增加該關鍵字的目的就在於:要透過關鍵字(key)——而非字元(token)——使普通的成員函式與其它形似的語法形式區別開來。這樣就有了 virtual,static, 以及 delegate 用來區分各種函式和形似函式的語法形式。

如果一個 delegate type 一次只搭載單獨一個方法(method),那它就可以搭載任意返回型別及形式的成員函式。然而,如果一個 delegate type 要同時搭載多個方法(methods),那麼返回型別就必須是 void。 例如,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 fo的形式(使用完整單字的冗長形式)列印當前的日期到標準輸出裝置:

Monday, February 26, 2001


非靜態方法(non-static method) announceTimeshort 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,後跟一對圓括弧(表示單個物件)或者方括號(表示陣列物件)。(在 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 中去。 // 見註釋 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)成員。例如 ,

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


下一步我們要為這個 delegate 成員提供讀寫訪問機制。在 C# 中,我們不要提供顯式的內聯方法(inline methods)用來讀寫非公有的資料成員。取而代之,我們為具名的屬性(named property)提供 getset 訪問符(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(屬性)之型別”的物件。在我們的例子中,valueAction 型別的物件。在執行期間,其被繫結到賦值表示式的右側。在下面的例子中,

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 方法被喚起時,通常我們並不知道 testHashtabletestArrayList 中哪一個的方法先被喚起;這取決於它們建構函式被喚起的順序。但我們可以知道的是,對於每個類別,其方法被喚起的順序就是方法被加入 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 不再引用該方法”的時候才會被施以垃圾收集處理。我們不用擔心物件會在自己眼皮底下被貿然清理掉了。壞訊息是,該物件將持續存在(譯註:這可能是不必要的),直到 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++ 中的卻不大相同。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# 內建陣列型別的語義。

經由 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 res is < An informative news group with Microsoft developer input dealing with both .NET and C# is . 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, , 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 <

致謝

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

註釋

[1] 對於 C++ 程式設計師來說,有兩點值得一題:(a) 需要在物件的型別名稱之後放一對圓括弧作為 default constructor,以及(b) 用於陣列下標的方括號要放在型別與陣列名稱之間。

[2] C# 中內建的陣列是一種由 .NET class library 提供的 Array class 之物件。Array class 的靜態方法和非靜態方法都可以被 C# 內建陣列物件使用。CopyToArray 的一個非靜態方法。

[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 provr of technical courses on Patterns, C++, C#, Java, , , 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 搭載。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752019/viewspace-982751/,如需轉載,請註明出處,否則將追究法律責任。

相關文章