[譯序:這是一篇古老的文章。但毫無(wú)疑問(wèn),Lippman對delegate的闡述是精辟的。]
如果你想拿 C# 與其它“C家族”的語(yǔ)言做比較,C# 正有個(gè)不同尋常的特性,其在 C++ 或者 Java 里沒(méi)有真正意義上的對應之物。
C# 是一個(gè)頗具爭議的新興語(yǔ)言,由 Microsoft 開(kāi)發(fā)創(chuàng )造,以作為其 Visual Studio.NET 的基石,目前正處于第一個(gè) Beta 版的發(fā)布階段。C# 結合了源自 C++ 和 Java 的許多特性。Java 社群對 C# 主要的批評在于,其聲稱(chēng) C# 只是一個(gè)蹩腳的 Java 克隆版本 ——與其說(shuō)它是語(yǔ)言創(chuàng )新的成果,倒不如說(shuō)是一樁訴訟的結果。而在 C++ 社群里,主要的批評(也同時(shí)針對 Java)是,C# 只不過(guò)是另一個(gè)泛吹濫捧的私有語(yǔ)言(yet another over-hyped proprietary language)。
本文意在展示一種 C# 的語(yǔ)言特性,而在 C++ 或 Java 中都沒(méi)有直接支持類(lèi)似的特性。這就是 C# 的 delegate 型別,其運作近似于一種指向成員函數的指針。我認為,C# delegate 型別是經(jīng)過(guò)深思熟慮的創(chuàng )新型語(yǔ)言特性,C++ 程序員(無(wú)論其對 C# 或者 Microsoft 有何想法)應該會(huì )對這個(gè)特性產(chǎn)生特殊的興趣。
為了激發(fā)討論,我將圍繞一個(gè) testHarness class 的設計來(lái)進(jìn)行闡述。這個(gè) testHarness class 能夠讓任何類(lèi)別對 static 或 non-static 的 class methods 進(jìn)行注冊,以便后續予以執行。Delegate 型別正是實(shí)現 testHarness class 的核心。
C# 的 Delegate Type
Delegate 是一種函數指針,但與普通的函數指針相比,區別主要有三:
1) 一個(gè) delegate object 一次可以搭載多個(gè)方法(methods)[譯注1],而不是一次一個(gè)。當我們喚起一個(gè)搭載了多個(gè)方法(methods)的 delegate,所有方法以其“被搭載到 delegate object 的順序”被依次喚起——稍候我們就來(lái)看看如何這樣做。
2) 一個(gè) delegate object 所搭載的方法(methods)并不需要屬于同一個(gè)類(lèi)別。一個(gè) delegate object 所搭載的所有方法(methods)必須具有相同的原型和形式。然而,這些方法(methods)可以即有 static 也有 non-static,可以由一個(gè)或多個(gè)不同類(lèi)別的成員組成。
3) 一個(gè) delegate type 的聲明在本質(zhì)上是創(chuàng )建了一個(gè)新的 subtype instance,該 subtype 派生自 .NET library framework 的 abstract base classes Delegate 或 MulticastDelegate,它們提供一組 public methods 用以詢(xún)訪(fǎng) delegate object 或其搭載的方法(methods)
聲明 Delegate Type
一個(gè) delegate type 的聲明一般由四部分組成:(a) 訪(fǎng)問(wèn)級別;(b) 關(guān)鍵字 delegate;(c)返回型別,以及該 delegate type 所搭載之方法的聲明形式(signature);(d) delegate type 的名稱(chēng),被放置于返回型別和方法的聲明形式(signature)之間。例如,下面聲明了一個(gè) public delegate type Action,用來(lái)搭載“沒(méi)有參數并具有 void 返回型別”的方法:
public delegate void Action();一眼看去,這與函數定義驚人的相似;唯一的區別就是多了 delegate 關(guān)鍵字。增加該關(guān)鍵字的目的就在于:要通過(guò)關(guān)鍵字(keyword)——而非字元(token)——使普通的成員函數與其它形似的語(yǔ)法形式區別開(kāi)來(lái)。這樣就有了 virtual,static, 以及 delegate 用來(lái)區分各種函數和形似函數的語(yǔ)法形式。
如果一個(gè) delegate type 一次只搭載單獨一個(gè)方法(method),那它就可以搭載任意返回型別及形式的成員函數。然而,如果一個(gè) delegate type 要同時(shí)搭載多個(gè)方法(methods),那么返回型別就必須是 void[譯注2]。 例如,Action 就可以用來(lái)搭載一個(gè)或者多個(gè)方法(method)。在 testHarness class 實(shí)現中,我們就將使用上述的 Action 聲明。
定義 Delegate Handle
在 C# 中我們無(wú)法聲明全局對象;每個(gè)對象定義必須是下述三種之一:局部對象;或者型別的對象成員;或者函數參數列表中的參數?,F在我只向你展示 delegate type 的聲明。之后我們再來(lái)看如何將其聲明為類(lèi)別中的成員。
C# 中的 delegate type 與 class, interface, 以及 array types 一樣,屬于 reference type。每個(gè) reference type 被分為兩部分:
- 一個(gè)具名的 句柄(named handle),由我們直接操縱;以及
- 一個(gè)該句柄所屬型別的不具名對象(unamed object),由我們通過(guò)句柄間接進(jìn)行操縱。必須經(jīng)由 new 顯式的創(chuàng )建該對象。
定義 reference type 是一個(gè)“兩步走”的過(guò)程。當我們寫(xiě):
Action theAction;的時(shí)候,theAction 代表“delegate type Action 之對象”的一個(gè) handle(句柄),其本身并非 delegate object。缺省情況下,它被設為 null。如果我們試圖在對其賦值(譯注:assigned,即與相應型別的對象做attachment)之前就使用它,會(huì )發(fā)生編譯期錯誤。例如,語(yǔ)句:
theAction();會(huì )喚起 theAction 所搭載的方法(method(s))。然而,除非它在定義之后、使用之前被無(wú)條件的賦值(譯注:assigned,即與相應型別的對象做attachment),否則該語(yǔ)句會(huì )引發(fā)編譯期錯誤并印出相關(guān)信息。
為 Delegate Object 分配空間
在這一節中,為了以最小限度的涉及面繼續進(jìn)行闡述,我們需要訪(fǎng)問(wèn)一個(gè)靜態(tài)方法(static method)和一個(gè)非靜態(tài)方法(non-static method),就此我采用了一個(gè) Announce class。該類(lèi)別的 announceDate 靜態(tài)方法(static method)以 long form 的形式(使用完整單字的冗長(cháng)形式)打印當前的日期到標準輸出設備:
Monday, February 26, 2001非靜態(tài)方法(non-static method) announceTime 以 short form 的形式(較簡(jiǎn)短的表示形式)打印當前時(shí)間到標準輸出設備:
00:58前兩個(gè)數字代表小時(shí),從午夜零時(shí)開(kāi)始計算,后兩個(gè)數字代表分鐘。Announce class 使用了由 .NET class framework 提供的 DateTime class。Announce 類(lèi)別的定義如下所示。
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 表達式創(chuàng )建一個(gè) Action delegate type(譯注:即創(chuàng )建一個(gè)該類(lèi)別的對象)。要搭載靜態(tài)方法,則傳入構造函數的引數由三部分組成:該方法所屬類(lèi)別的名稱(chēng);方法的名稱(chēng);分隔兩個(gè)名稱(chēng)用的 dot operator(.):
theAction = new Action( Announce.announceDate );要搭載非靜態(tài)方法,則傳入構造函數的引數也由三部分組成:該方法所屬的類(lèi)別對象名稱(chēng);方法的名稱(chēng);分隔兩個(gè)名稱(chēng)用的 dot operator(.):
Announce an = new Announce();theAction = new Action( an.announceTime );可以注意到, theAction 被直接賦值,事先沒(méi)有做任何檢查(比如,檢查它是否已經(jīng)指代一個(gè)堆中的對象,如果是,則先刪除該對象)。在 C# 中,存在于 managed heap(受托管的堆)中的對象由運行期環(huán)境對其施以垃圾收集動(dòng)作(garbage collected)。我們不需要顯式的刪除那些經(jīng)由 new 表達式分配的對象。
在程序的 managed heap(受托管的堆)中,new 表達式既可以為獨個(gè)對象做分配
HelloUser myProg = new HelloUser();也可以為數組對象做分配
string [] messages = new string[ 4 ];分配語(yǔ)句的形式為:型別的名稱(chēng),后跟關(guān)鍵字 new,后跟一對圓括?。ū硎締蝹€(gè)對象)或者方括號(表示數組對象)[1]。(在 C# 語(yǔ)言設計中的一個(gè)普遍特征就是,堅持使用單一明晰的形式來(lái)區別不同的功用。)
一個(gè)快速的概覽:Garbage Collection(垃圾收集)
如下述數組對象所示,當我們在 managed heap(受托管的堆)中為 reference type 分配了空間:
int [] fib = new int[6]{ 1,1,2,3,5,8 };對象自動(dòng)的維護“指向它的句柄(handles)”之數目。在這個(gè)例子中,被 fib 所指向的數組對象有一個(gè)關(guān)聯(lián)的引用計數器被初始化為1。如果我們現在初始化另一個(gè)句柄,使其指向 fib 所指代的數組對象:
int [] notfib = fib;這次初始化導致了對 fib 所指代數組對象的一次 shallow copy(淺層拷貝)。這就是說(shuō),notfib 現在也指向 fib 所指向的數組對象。該數組對象所關(guān)聯(lián)的引用計數變成了2。
如果我們經(jīng)由 notfib 修改了數組中某個(gè)元素,比如
notfib [ 0 ] = 0;這個(gè)改變對于 fib 也是可見(jiàn)的。如果這種對同一個(gè)對象的多重訪(fǎng)問(wèn)方式并非所需,我們就需要編寫(xiě)代碼,做一個(gè) deep copy(深層拷貝)。例如,
// 分配另一個(gè)數組對象notfib = new int [6];// 從 notfib 的第0個(gè)元素開(kāi)始,// 依次將 fib 中的元素拷貝到 notfib 中去。// 見(jiàn)注釋 [2]fib.CopyTo( notfib, 0 );notfib 現在并不指代 fib 所指代的那個(gè)對象了。先前被它們兩個(gè)同時(shí)指向的那個(gè)對象將其關(guān)聯(lián)的引用計數減去1。notfib 所指代對象的初始引用計數為1。如果我們現在也將 fib 重新賦值為一個(gè)新的數組對象——例如,一個(gè)包含了Fibonacci數列前12個(gè)數值的數組:
fib = new int[12]{ 1,1,2,3,5,8,13,21,34,55,89,144 };對于之前被 fib 所指代的那個(gè)數組對象,其現在的引用計數變成了0。在 managed heap(受托管的堆)中,當垃圾收集器(garbage collector)處于活動(dòng)狀態(tài)時(shí),引用計數為0的對象被其作上刪除標記。
定義 Class Properties
現在讓我們將 delegate object 聲明為 testHarness class 的一個(gè)私有靜態(tài)(private static)成員。例如 [3],
public class testHarness{public delegate void Action();static private Action theAction;// ...}下一步我們要為這個(gè) delegate 成員提供讀寫(xiě)訪(fǎng)問(wèn)機制。在 C# 中,我們不要提供顯式的內聯(lián)方法(inline methods)用來(lái)讀寫(xiě)非公有的數據成員。取而代之,我們?yōu)榫呙?i>屬性(named property)提供 get 和 set 訪(fǎng)問(wèn)符(accessors)。下面是個(gè)簡(jiǎn)單的 delegate property。我們不妨將其稱(chēng)為 Tester:
public class testHarness{static public Action Tester{get{ return theAction; }set{ Action = value; }}// ...}Property(屬性)既可以封裝靜態(tài)數據成員,也可以封裝非靜態(tài)數據成員。Tester 就是 delegate type Action 的一個(gè) static property(靜態(tài)屬性)。(可以注意到。我們將 accessor 定義為一個(gè)代碼區塊。編譯器內部由此產(chǎn)生 inline method。)
get 必須以 property(屬性)的型別作為返回型別。在這個(gè)例子中,其直接返回所封裝的對象。如果采用“緩式分配(lazy allocation)”,get 可以在初次被喚起的時(shí)候建構并存放好對象,以便后用。
類(lèi)似的,如果我們希望 property(屬性)能夠支持寫(xiě)入型訪(fǎng)問(wèn),我們就提供 set accessor。set 中的 value 是一個(gè)條件型關(guān)鍵字(conditional-keyword)。也就是說(shuō),value 僅在 set property 中具有預定義的含義(譯注:也就是說(shuō),value 僅在 set 代碼段中被看作一個(gè)關(guān)鍵字):其總是代表“該 property(屬性)之型別”的對象。在我們的例子中,value 是 Action 型別的對象。在運行期間,其被綁定到賦值表達式的右側。在下面的例子中,
Announce an = new Announce();testHarnes.Tester =new testHarness.Action( an.announceTime );set 以?xún)嚷?lián)(inline)的方式被展開(kāi)到 Tester 出現的地方。value 對象被設置為由 new 表達式返回的對象。
喚起 Delegate Object
如之前所見(jiàn),要喚起由 delegate 所搭載的方法,我們對 delegate 施加 call operator(圓括弧對):
testHarness.Tester();這一句喚起了Tester property 的 get accessor;get accessor返回 theAction delegate handle。如果 theAction 在此刻并未指向一個(gè) delegate object,那么就會(huì )有異常被拋出。從類(lèi)別外部實(shí)行喚起動(dòng)作的規范做法(delegate-test-and-execute,先實(shí)現代理,再測試,最后執行之)如下所示:
if ( testHarness.Tester != null )testHarness.Tester();對于 testHarness class,我們的方法只簡(jiǎn)單的封裝這樣的測試:
static public void run(){if ( theAction != null )theAction();}關(guān)聯(lián)多個(gè) Delegate Objects
要讓一個(gè) delegate 搭載多個(gè)方法,我們主要使用 += operator 和 -= operator。例如,設想我們定義了一個(gè) testHashtable class。在構造函數中,我們把各個(gè)關(guān)聯(lián)的測試加入到 testHarness 中:
public class testHashtable{public void test0();public void test1();testHashtable(){testHarness.Tester += new testHarness.Action( test0 );testHarness.Tester += new testHarness.Action( test1 );}// ...}同樣,如果我們定義一個(gè) testArrayList class,我們也在 default constructor 中加入關(guān)聯(lián)的測試??梢宰⒁獾?,這些方法是靜態(tài)的。
public class testArrayList{static public void testCapacity();static public void testSearch();static public void testSort();testArrayList(){testHarness.Tester += newtestHarness.Action(testCapacity);testHarness.Tester += new testHarness.Action(testSearch);testHarness.Tester += new testHarness.Action(testSort);}// ...}當 testHarness.run 方法被喚起時(shí),通常我們并不知道 testHashtable 和 testArrayList 中哪一個(gè)的方法先被喚起;這取決于它們構造函數被喚起的順序。但我們可以知道的是,對于每個(gè)類(lèi)別,其方法被喚起的順序就是方法被加入 delegate 的順序。
Delegate Objects 與 Garbage Collection(垃圾收集)
考察下列局部作用域中的代碼段:
{Announce an = new Announce();testHarness.Tester +=new testHarness.Action( an.announceTime );}當我們將一個(gè)非靜態(tài)方法加入到 delegate object 中之后,該方法的地址,以及“用來(lái)喚起該方法,指向類(lèi)別對象的句柄(handle)”都被存儲起來(lái)。這導致該類(lèi)別對象所關(guān)聯(lián)的引用計數自動(dòng)增加。
an 經(jīng)由 new 表達式初始化之后,managed heap(受托管的堆)中的對象所關(guān)聯(lián)的引用計數被初始化為1。當 an 被傳給 delegate object 的構造函數之后,Announce 對象的引用計數增加到2。走出局部作用域之后,an 的生存期結束,該引用計數減回到1——delegate object還占用了一個(gè)。
好消息是,如果有一個(gè) delegate 引用了某對象的一個(gè)方法,那么可以保證該對象會(huì )直到“delegate object 不再引用該方法”的時(shí)候才會(huì )被施以垃圾收集處理[4]。我們不用擔心對象會(huì )在自己眼皮底下被貿然清理掉了。壞消息是,該對象將持續存在(譯注:這可能是不必要的),直到 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 的初始想法是,實(shí)現一個(gè)析構函數用以移除在構造函數中加入的測試用方法。然而,C# 中的析構函數調用機制與 C++ 中的卻不大相同[5]。C# 的析構函數既不會(huì )因為對象生存期結束而跟著(zhù)被喚起,也不會(huì )因為釋放了對象最后一個(gè)引用句柄( reference handle)而被直接喚起。事實(shí)上,析構函數僅在垃圾收集器作垃圾收集時(shí)才被調用,而施行垃圾收集的時(shí)機一般是無(wú)法預料的,甚至可以根本就沒(méi)施行垃圾收集。
C# 規定,資源去配動(dòng)作被放進(jìn)一個(gè)稱(chēng)為 Dispose 的方法中完成,用戶(hù)可以直接調用該方法:
public void Dispose (){testHarness.Tester -= new testHarness.Action( test0 );testHarness.Tester -= new testHarness.Action( test1 );}如果某類(lèi)別定義了一個(gè)析構函數,其通常都會(huì )喚起 Dispose。
訪(fǎng)問(wèn)底層的類(lèi)別接口
讓我們再回頭看看先前的代碼:
{Announce an = new Announce();Action act =new testHarness.Action( an.announceTime );testHarness.Tester +=
聯(lián)系客服