1. 程式人生 > >C# in Depth學習筆記-委託

C# in Depth學習筆記-委託

2.1 委託

委託在某種程度上提供了間接的方法。換言之,不需要直接指定一個要執行的行為,而是將這個行為用某種方式“包含”在一個物件中。

這個物件可以像其他任何物件那樣使用。在該物件中,可以執行封裝的操作。

可以選擇將委託型別看做只定義了一個方法的介面,將委託的例項看做實現了那個介面的一個物件。

讓我們以遺囑為例。遺囑由一系列指令組成,比如:“付賬單,捐善款,其餘財產留給貓。”

遺囑一般是在某人去世之前寫好,然後把它放到一個安全的地方。去世後,(希望)律師會執行這些指令。

C#中的委託和現實世界的遺囑一樣,也是要在恰當的時間執行一系列操作。

如果程式碼想要執行操作,但不知道操作細節,一般可以使用委託。

例如, Thread 類之所以知道要在一個新執行緒裡執行什麼,唯一的原因就是在啟動新執行緒時,向它提供了一個 ThreadStart 或 ParameterizedThreadStart 委託例項。


2.1.1 簡單委託的構成

為了讓委託做某事,必須滿足4個條件:

① 宣告委託型別

②必須有一個方法包含了要執行的程式碼

③必須建立一個委託例項

④必須呼叫(invoke)委託例項

下面依次討論上述每一步。


1. 宣告委託型別

委託型別實際上只是引數型別的一個列表以及一個返回型別。它規定了型別的例項能表示的操作。

例如,以如下方式宣告一個委託型別:

delegate
void StringProcessor(string input);

上述程式碼指出,如果要建立 StringProcessor 的一個例項,需要只帶一個引數(string)的方法,而且這個方法要有一個 void 返回型別(什麼都不返回)。

這裡的重點在於, StringProcessor 其實是一個從 System.MulticastDelegate 派生的型別,後者又派生自 System.Delegate 。

它有方法,可以建立它的例項,並將引用傳遞給例項,所有這些都沒有問題。

雖然它有一些自己的“特性”,但假如你對特定情況下發生的事情感到困惑,那麼首先想一想使用“普通”的引用型別時發生的事情。

討論委託的下一個基本元素時,會用到 StringProcessor 委託型別。

混亂的根源:容易產生歧義的“委託”

委託經常被人誤解,這是由於大家喜歡用委託這個詞來描述委託型別和委託例項。

這兩者的區別其實就是任何一個型別和該型別的例項的區別。

例如, string 型別本身和一組特定的字元肯定不同。委託型別和委託例項這兩個詞會貫穿本章始終,從而讓你明白我具體說的是什麼。


2. 為委託例項的操作找到一個恰當的方法

我們的下一個基本元素是找到(或者寫)一個方法,它能做我們想做的事情,同時具有和委託型別相同的簽名。

基本的思路是,要確保在呼叫(invoke)一個委託例項時,使用的引數完全匹配,而且能以我們希望的方式(就像普通的方法呼叫)使用返回值(如果有的話)。

看看以下 StringProcessor 例項的5個備選方法簽名:

void PrintString(string x)
void PrintInteger(int x)
void PrintTwoStrings(string x,string y)
int GetStringLength(string x)
void PrintObject(object x)

第1個方法完全符合要求,所以可以用它建立一個委託例項。

第2個方法雖然也有一個引數,但不是 string 型別,所以不相容 StringProcessor 。

第3個方法第1個引數的型別匹配,但引數數量不匹配,所以也不相容。

第4個方法有正確的引數列表,但返回型別不是 void 。(如果委託型別有返回型別,方法的返回型別也必須與之匹配。)

第5個方法比較有趣,任何時候呼叫一個 StringProcessor 例項,都可以呼叫具有相同的引數的 PrintObject 方法,這是由於 string 是從 object 派生的。

把這個方法作為 StringProcessor 的一個例項來使用是合情合理的,但C# 1要求委託必須具有完全相同的引數型別。C# 2改善了這個狀況——詳見第5章。

在某些方面,第4個方法也是相似的,因為總是可以忽略不需要的返回值。

然而, void 和非 void 返回型別目前一直被認為是不相容的。

部分原因是因為系統的其他方面(特別是JIT)需要知道,在執行方法時返回值是否會留在棧上。

假定有一個針對相容的簽名( PrintString )的方法體。接著,討論下一個基本元素——委託例項本身。


3. 建立委託例項

有了一個委託型別和一個有正確簽名的方法後,接著可以建立委託型別的一個例項,指定在呼叫委託例項時就執行該方法。作者將該方法稱為委託例項的操作。

至於具體用什麼形式的表示式來建立委託例項,取決於操作使用例項方法還是靜態方法。

假定 PrintString 是 StaticMethods 型別中的一個靜態方法,在 InstanceMethods 型別中是一個例項方法。

下面是建立一個 StringProcessor 例項的兩個例子:

//靜態方法
StringProcessor pro1,proc2;
proc1 = new StringProcessor(StaticMethods.PrintString);
//例項方法
InstanceMethods instance = new InstanceMethods();
proc2 = new StringProcessor(instance.PrintString);

如果操作是靜態方法,指定型別名稱就可以了。如果操作是例項方法,就需要先建立型別(或者它的派生型別)的一個例項。這和平時呼叫方法是一樣的。

這個物件稱為操作的目標。呼叫委託例項時,就會為這個物件呼叫(invoke)方法。

如果操作在同一個類中(這種情況經常發生,尤其是在UI程式碼中寫事件處理程式時),那麼兩種限定方式都不需要——例項方法隱式將 this 引用作為字首。

同樣,這些規則和你直接呼叫方法時沒什麼兩樣。單純建立一個委託例項卻不在某一時刻呼叫它是沒有什麼意義的。看看最後一步——呼叫。

最終的垃圾(或者不是,視情況而定)

必須注意,假如委託例項本身不能被回收,委託例項會阻止它的目標被作為垃圾回收。這可能造成明顯的記憶體洩漏(leak),尤其是假如某“短命”物件呼叫了一個“長命”物件中的事件,並用它自身作為目標。“長命”物件間接容納了對“短命”物件的一個引用,延長了“短命”物件的壽命。


4. 呼叫委託例項

這是很容易的一件事兒,呼叫一個委託例項的方法就可以了。這個方法本身被稱為 Invoke 。

在委託型別中,這個方法以委託型別的形式出現,並且具有委託型別宣告中指定的相同引數列表和返回型別。

所以,在我們的例子中,有一個像下面這樣的方法:

呼叫 Invoke 會執行委託例項的操作,向它傳遞在呼叫 Invoke 時指定的任何引數。另外,如果返回型別不是 void ,還要返回操作的返回值。

C#將這個過程變得更簡單——如果有一個委託型別的變數,就可以把它視為方法本身。

觀察由不同時間發生的事件構成的一個事件鏈,很容易就可以理解這一點,如圖2-1所示。

就是這麼簡單。所有原料都已齊備,接著將CLR預熱到200℃,將所有東西都攪拌到一起,看看會發生什麼。


5. 一個完整的例子和一些動機

//程式碼清單2-1 以各種簡單的方式使用委託
delegate void StringProcessor(string input);//宣告委託型別
class Person
{
    string name;
    public Person(string name) { this.name = name; }
    //宣告相容的例項方法
    public void Say(string message)
    {
        Console.WriteLine("{0}說:{1}",name,message);
    }
}
class BackGround
{
    //宣告相容的靜態方法
    public static void Note(string note)
    {
        Console.WriteLine("({0})",note);
    }
}
class Program
{
    static void Main()
    {
        //宣告人物例項
        Person jon = new Person("鐵子");
        Person tom = new Person("楊樹");
        //宣告委託例項,並新增對應的例項方法和靜態方法
        StringProcessor jonsVoice, tomsVoice, background;
        jonsVoice = new StringProcessor(jon.Say);
        tomsVoice = new StringProcessor(tom.Say);
        background = new StringProcessor(BackGround.Note);
        //呼叫委託例項
        jonsVoice("你瞅啥?");
        tomsVoice.Invoke("瞅你咋地?");
        background("燒烤攤又開始洋溢著歡快的聲音了");
        Console.Read();
    }
}

首先宣告委託型別 ,接著建立兩個方法,它們都與委託型別相容。

一個是例項方法( Person.Say ),另一個是靜態方法( Background.Note ),這樣就可以看到在建立委託例項時 ,它們在使用方式上的區別。

程式碼清單2-1建立了 Person 類的兩個例項,便於觀察委託目標所造成的差異。

jonsVoice 被呼叫時 它會呼叫 name 為 Jon 的那個 Person 物件的 Say 方法。

同樣,tomsVoice 被呼叫時使用的是 name 為 Tom 的物件。

然後展示了呼叫委託例項的兩種方式,顯式呼叫 Invoke 和使用C#的簡化形式。一般情況下只需使用簡化形式。以下是程式碼清單2-1的輸出:

鐵子說:你瞅啥?
楊樹說:瞅你咋地?
(燒烤攤又開始洋溢著歡快的聲音了)

使用委託的意義

如果僅僅是為了顯示上述3行輸出,程式碼清單2-1的程式碼未免太多了。即使想要使用Person 類和 Background 類,也沒有必要使用委託。

但是我們不能僅僅由於你希望某事發生,就意味著你始終會在正確的時間和地點出現,並親自使之發生。有時,你需要給出一些指令,將職責委託給別人。

應該強調的一點是,在軟體世界中,沒有物件“留遺囑”這樣的事情發生。經常都會發現這種情況:委託例項被呼叫時,最初建立委託例項的物件仍然是“活蹦亂跳”的。

相反,委託相當於指定一些程式碼在特定的時間執行,那時,你也許已經無法(或者不想)更改要執行的程式碼。

如果我希望在單擊一個按鈕後發生某事,但不想對按鈕的程式碼進行修改,我只是希望按鈕呼叫我的某個方法,那個方法能執行恰當的操作。

委託的實質是間接完成某種操作,事實上,許多面向物件程式設計技術都在做同樣的事情。我們看到,這增大了複雜性,但同時也增加了靈活性。

現在已經對簡單委託有了更多的理解,接著看看如何將委託合併到一起,以便成批地執行操作,而不是隻執行一個。


2.1.2 合併和刪除委託

委託例項實際有一個操作列表與之關聯。這稱為委託例項的呼叫列表(invocation list)。

System.Delegate 型別的靜態方法 Combine 和 Remove 負責建立新的委託例項。

其中,Combine 負責將兩個委託例項的呼叫列表連線到一起,而 Remove 負責從一個委託例項中刪除另一個例項的呼叫列表。

委託是不易變的

建立了委託例項後,有關它的一切就不能改變。這樣一來,就可以安全地傳遞委託例項的引用,並把它們與其他委託例項合併,同時不必擔心一致性、執行緒安全性或者是否有其他人試圖更改它。

在這一點上,委託例項和 string 是一樣的。string的 實 例 也 是 不 易 變 的 。 之 所 以 提 到 string , 是 因 為 Delegate.Combine 和String.Concat 很像——都是合併現有的例項來形成一個新例項,同時根本不更改原始物件。對於委託例項,原始呼叫列表被連線到一起。注意,如果試圖將 null 和委託例項合併到一起, null 將被視為帶有空呼叫列表的一個委託。

使用操作符代替兩個方法

很少在C#程式碼中看到對 Delegate.Combine 的顯式呼叫,一般都是使用+和+=操作符。

圖2-2展示了轉換過程,其中 x 和 y 都是相同(或相容)委託型別的變數。所有轉換都由C#編譯器完成。

 

可以看出,這是一個相當簡單的轉換過程,但它使程式碼變得整潔多了。

Delegate.Remove 方法從一個例項中刪除另一個例項的呼叫列表。C#使用-和-=運算子簡寫形式的方法非常簡單,一看便知。

Delegate.Remove(source, value) 將建立一個新的委託例項,其呼叫列表來自 source , value 中的列表則被刪除。如果結果有一個空的呼叫列表,就返回 null 。

委託返回非void操作中最後一個操作的返回值

呼叫委託例項時,它的所有操作都順序執行。如果委託的簽名具有一個非 void 的返回型別,則 Invoke 的返回值是最後一個操作的返回值。

很少有非 void 的委託例項在它的呼叫列表中指定多個操作,因為這意味著其他所有操作的返回值永遠都看不見。除非每次呼叫程式碼使用Delegate.GetInvocationList 獲取操作列表時,都顯式呼叫某個委託。

任何操作異常立都會即終止委託

如果呼叫列表中的任何操作丟擲一個異常,都會阻止執行後續的操作。例如,假定呼叫一個委託例項,它的操作列表是 [a, b, c] ,但操作 b 丟擲了一個異常,這個異常會立即“傳播”,操作 c 不會執行。

進行事件處理時,委託例項的合併與刪除會特別有用。既然我們已經理解了合併與刪除涉及的操作,就很容易理解事件。


2.1.3 對事件的簡單討論

事件的基本思想是讓程式碼在發生某事時作出響應,如在正確單擊一個按鈕後儲存一個檔案。在這個例子中,事件是“單擊按鈕”,操作是“儲存檔案”。

開發者經常將事件和委託例項,或者將事件和委託型別的欄位混為一談。

但它們之間的差異十分大:事件不是委託型別的欄位。之所以產生混淆,原因和以前相同,因為C#提供了一種簡寫方式,允許使用欄位風格的事件(field-like event)。

先從C#編譯器的角度看看事件到底由什麼組成。


將事件看成屬性

將事件看做類似於屬性(property)的東西是很有好處的。

宣告為具有一種特定的型別

兩者都宣告為具有一種特定的型別。對於事件來說,必須是一個委託型別。

能獲取或設定欄位/方法

使用屬性時,實際是在呼叫方法,也就是取值方法和賦值方法。實現屬性時,可以在那些方法中做任何事情。

同樣,在訂閱或取消訂閱一個事件時,看起來就像是在通過 += 和 -= 運算子使用委託型別的欄位。

但和屬性的情況一樣,這個過程實際是在呼叫方法。對於一個純粹的事件,你所能做的事情就是訂閱或者取消訂閱。

最終是由事件方法來做真正有用的事情,如找到你試圖新增和刪除的事件處理程式,並使它們在類中的其他地方可用。

使用封裝實現釋出/訂閱

事件存在的首要理由和屬性差不多,它們都添加了一個封裝層以實現釋出/訂閱模式。

通常,我們不希望其他程式碼能直接設定欄位值。最起碼也要先由所有者(owner)對新值進行證。

同樣,我們通常不希望類外部的程式碼隨意更改(或呼叫)一個事件的處理程式。

類能通過新增方法的方式來提供額外的訪問。例如,可以重置事件的處理程式列表,或者引發事件。但是如果只對外揭示事件本身,類外部的程式碼就只能新增和刪除事件處理程式。

欄位風格的事件使所有這些的實現變得更易閱讀,只需一個宣告就可以了。

編譯器會將宣告轉換成一個具有預設 add / remove 實現的事件和一個私有委託型別的欄位。類內的程式碼能看見欄位;類外的程式碼只能看見事件。

這樣一來,表面上似乎能呼叫一個事件,但為了呼叫事件處理程式,實際做的事情是呼叫儲存在欄位中的委託例項。