重中之重:委托與事件


相關文章鏈接

編程之基礎:數據類型(一)

編程之基礎:數據類型(二)

高屋建瓴:梳理編程約定

動力之源:代碼中的泵

難免的尷尬:代碼依賴

可復用代碼:組件的來龍去脈

物以類聚:對象也有生命

重中之重:委托與事件

委托是.NET編程中的重點之一,委托的作用簡單概括起來就是"調用方法"。使用委托,我們可以異步(同步)調用方法、一次調用多個方法甚至可以將方法作為參數傳遞給別人供別人回調。程序的運行過程便是方法之間的調用過程,所以委托是.NET開發者必須掌握的知識點之一。.NET編程中的事件建立在委托的基礎之上,要掌握事件的用法必須先了解委托。

5.1 什么是.NET中的委托

委托的字面意思為"把什么什么東西托付給某某人去做",偏向於一個動作,但是在.NET中,委托卻是一個名詞,表示"代理"或者"中間人"的意思。A本來要找B辦事,但是它沒有直接找B,而是托付給C,讓C去找B把事兒給辦了,如果按照"委托"字面意思去理解,"A找C"的這個行為叫"委托",但是在.NET中,C這個人叫"委托"。

 

圖5-1 .NET中委托含義

圖5-1中A表示請求辦事情的人(請求方),B是最終處理事情的人(應答方),C表示.NET中的委托。既然C是中間人,那么它肯定包含有B的一些信息,不然怎么去找B辦事情?

注:本書中之后出現的所有與"委托"有關的詞匯均指.NET中的委托,也就是圖5-1中的C部分。另外,按照第二章中所講的內容,A可以稱為ClientB則稱為Server

5.1.1 委托的結構

委托的職責就是代替請求方去找應答方辦事情,在程序中,體現為調用應答方的方法,換句話說,委托其實就是起到"調用方法"的作用。

程序中調用一個方法的必備條件是:知道要調用的方法,知道這個方法的所有者(如果該方法為實例方法)。因此一個委托中至少要包含圖5-2中的信息:

圖5-2 委托組成

圖5-2中顯示一個委托的結構組成,它至少包含要調用的方法Method和方法的所有者Target(如果方法為靜態方法,Target為null)。也就是說,委托是一種數據結構,我們可以把它看作是一種類型,類型里面包含一些成員。事實上,.NET中的委托就是一種類型,有着共同的基類Delegate,我們程序中定義的各種各樣的委托都是從該類派生而來。

注:我們使用到的委托類型都派生自MulticastDelegate類,后者再派生自Delegate類型。系統不允許我們像定義普通類型的方式顯式從這兩個類型派生出新的委托,只能使用一種特殊定義類型的方法(后面有講到)。另外,我們平時常說的"委托"是指一個委托類型的對象,本書中可以根據上下文判斷"委托"是指委托類型還是委托類型的對象。

由於每個方法的簽名不一樣,因此一種委托只能負責調用一種類型的方法,也就是說,我們在定義委托類型的時候,必須提供它能夠調用方法的簽名,因此,.NET中規定,以如下形式去定義一個委托類型:

1 //Code 5-1
2 public delegate void DelegateName(object[] arg1);

像普通聲明一個方法一樣,提供方法名稱、參數、訪問修飾符以及返回值,然后在前面加上delegate關鍵字,這樣就定義了一個委托類型,委托類型名稱為DelegateName,它能夠調用返回值為void,帶有一個object[]類型參數的所有方法(包括實例方法和靜態方法)。換句話說,就是所有符合該簽名的方法都可以由DelegateName委托調用。注意我們不能顯式在代碼中這樣去定義一個委托類型:

1 //Code 5-2
2 public class DelegateName:MulticastDelegate
3 {
4     //
5 }

編譯器不允許以上代碼Code 5-2通過編譯。

注:"方法簽名"指方法的參數個數、參數類型以及返回值等,具有相同簽名的兩個方法參數列表一致,返回值一致(名稱可以不一樣),int fun1(string a,int b)int fun2(string b,int a)兩個方法的簽名相同。

委托類型定義完成后,怎么去實例化一個委托對象呢?其實很簡單,跟實例化其它類型對象一樣,我們可以通過new關鍵字,

 1 //Code 5-3
 2 class Calculate
 3 {
 4     public Calculate()
 5     {
 6         //
 7     }
 8     public int DoDivision(int first,int second) //NO.1
 9     {
10         return first/second;
11     }
12 }
13 private delegate int DivisionDelegate(int arg1,int arg2); //NO.2
14 class Program
15 {
16     static void Main()
17     {
18         Calculate c = new Calculate();
19         DivisionDelegate d = new DivisionDelegate(c.DoDivision); //NO.3
20         int result = d(10,5); // int result = c.DoDivision(10,5); NO.4
21         Console.WriteLine("the result is " + result);
22     }
23 }

代碼Code 5-3中我們定義了一個Calculate類型,專門負責除法運算(NO.1處),定義了一個DivisionDelegate委托(NO.2處)。在實際計算的時候,我們並沒有直接調用Calculate類的DoDivision方法,而是先新建了一個委托對象d(NO.3處),給d的構造方法傳遞一個參數c.DoDivision。之后,我們通過這個委托d來計算10除以5的值(NO.4處)。整個過程中,我們沒有直接使用對象c,而是通過委托d,這就像本節剛開始所說的:委托的職責就是代替請求方(Program類)去找應答方(c對象)辦事情(除法運算)。代碼中委托對象d的結構如下圖5-3:

圖5-3 委托對象d內部結構

圖5-3中顯示,委托中的Target指向c對象,Method指向c對象的DoDivision方法,委托對象d就是對c.DoDivision(int,int)的一個封裝。

另外,在我們使用new關鍵字創建委托實例時,會給它的構造方法傳遞了一個參數,該參數為一個方法名稱。如果是實例方法,就應該使用"對象.方法名稱"這樣的格式(注意如果在同一個類中,對象默認為this,可以省略),如果是靜態方法,就應該使用"類名稱.方法名稱"這樣的格式(如果在同一個類中,類名稱可以省略)。給構造方法傳遞的這個參數其實就是用來初始化委托內部的Target和Method兩個成員。使用委托調用方法時,我們直接使用"委托對象(參數列表);"這樣的格式即可,它等效於"委托對象.Invoke(參數列表)"。

注:給委托賦值的另外一種方式是:委托對象=方法。代碼Code 5-3中賦值部分可以換成DivisionDelegate d = c.DoDivision;,含義跟用new關鍵字一樣。另外,每一個自定義委托類型都包含一個Invoke方法,它的作用就是調用方法(與BeginInvoke方法對應,詳見本書第六章),"委托對象(參數列表)"只是調用方法的一種簡寫方式。

委托內部的Target為Object類型,表示方法的所有者,Method為MethodInfo類型,表示一個方法。通過委托調用方法"int result = d(10,5);",委托內部相當於:

1 //Code 5-4
2 int result = (int)Method.Invoke(Target,new Object[]{10,5});

意思就是在指定的對象(Target)上調用指定的方法(Method)。

5.1.2 委托鏈表

上一小節中提到的委托都是單委托,它只對一個方法進行封裝,也就是說,使用單委托只能調用一個方法。

之前提到過,一個委托應該可以調用多個方法,只要這些方法的簽名與該委托一致,那么怎樣讓一個委托同時調用兩個或者兩個以上的方法呢? 我們代碼中很好實現,直接使用加法賦值運算符(+=)將多個方法附加到委托對象上,

 1 //Code 5-5
 2 class Program
 3 {
 4     static void Fun1(object sender,EventArgs e)
 5     {
 6         //
 7         Console.WriteLine("Call Fun1");
 8     }
 9     static void Fun2(object sender,EventArgs e)
10     {
11         //
12         Console.WriteLine("Call Fun2");
13     }
14     static void Fun3(object sender,EventArgs e)
15     {
16         //...
17         Console.WriteLine("Call Fun3");
18     }
19     static void Main()
20     {
21         EventHandler eh = new EventHandler(Fun1); //NO.1
22         eh += Fun2; //NO.2
23         eh += new EventHandler(Fun3); //NO.3
24         eh -= Fun2; //NO.4
25         eh(null,null); //NO.5
26         // print out:
27         // Call Fun1
28         // Call Fun3
29     }
30 }

代碼Code 5-5中定義了一個EventHandler委托對象eh(NO.1處),按照先后順序依次使用加法賦值運算符(+=)給它附加Fun2和Fun3方法(NO.2和NO.3處),然后使用減法賦值運算符(-=)移除Fun2方法(NO.4處),最后通過委托調用方法,依次輸出"Call Fun1"和"Call Fun3"。由此可以得出三個結論:

(1)一個委托對象確實可以調用多個方法;

(2)這些方法可以按照附加順序先后依次調用;

(3)可以從委托對象上移除一個方法,不影響其它方法。

注:確切的說,應該是將委托附加到委托對象上,另外代碼中使用的都是靜態方法,這時候委托內部Targetnull+=-=運算符相當於Delegate類的靜態方法Delegate.CombineDelegate.Remove,專門負責附加或移除委托操作。

根據以上三個結論,我們很有必要了解一下委托內部到底是怎樣管理附加到它上面的方法,換句話說,委托內部到底有怎樣的數據結構來組織和調用這些方法?

在學習數據結構中的"鏈表"時我們知道,每一個鏈表節點(Node)的結構都是相同的。鏈表表頭、鏈表表尾以及中間的節點本質上是沒有任何區別,我們可以將任意一個(或一串)節點附加到已有的一個(或一串)節點后面,從而形成一個更長的節點串。我們還能通過鏈表表頭訪問整個鏈表中的每一個節點(通過Next成員)。總之,只要知道了任意一個節點,我們就能訪問該節點后面的所有節點(注意這里指的是單向鏈表)。單向鏈表結構類似如下圖5-4:

圖5-4 單向鏈表結構

圖5-4中實線矩形方框表示單向鏈表中的一個節點,所有節點都屬於同一類型對象,因此結構相同。節點類Node代碼類似如下:

 1 //Code 5-6
 2 class Node
 3 {
 4     private string _name; //node's name
 5     private Node _next; // the next node
 6     public string Name     
 7     {
 8         get
 9         {
10             return _name;
11         }
12         set
13         {
14             _name = value;
15         }
16     }
17     public Node Next
18     {
19         get
20         {
21             return _next;
22         }
23         set
24         {
25             _next = value;
26         }
27     }
28     public Node(string name)
29     {
30         _name = name;
31     }
32     public int GetNodesCount() //get the nodes' count from this to the end
33     {
34         int count = 0;
35         Node tmp = this;
36         do
37         {
38             count++;
39             tmp = tmp.Next;
40         }
41         while(tmp != null)
42         return count;
43     }
44     public Node[] GetNodesList() //get all nodes from this to the end
45     {
46         Node[] nodes = new Node[GetNodes()];
47         int index = -1;
48         Node tmp = this;
49         do
50         {
51             index++;
52             nodes[index] = tmp;
53             tmp = tmp.Next;
54         }
55         while(tmp != null)
56         return nodes;
57     }
58     public void ShowMyInfo() //show node's info
59     {
60         Console.WriteLine("My name is " + _name);
61     }
62     public void ShowInfo() //show the all nodes' info from this to the end
63     {
64         ShowMyInfo();
65         if(Next != null)
66         {
67             Next.ShowInfo();
68         }
69     }
70 }
71 class Program
72 {
73     static void Main()
74     {
75         Node node1 = new Node("node1");
76         Node node2 = new Node("node2");
77         Node node3 = new Node("node3");
78         node1.Next = node2; //NO.1
79         node2.Next = node3; //NO.2
80         Console.WriteLine("the count of the nodes from node1 to the end:" + node1.GetNodesCount()); //NO.3
81         Console.WriteLine("the count of the nodes from node2 to the end:" + node2.GetNodesCount());
82         Console.WriteLine("the count of the nodes from node3 to the end:" + node3.GetNodesCount());
83         Node[] nodes = node1.GetNodesList(); //NO.4
84         foreach(Node n in nodes)
85         {
86             n.ShowMyInfo(); //NO.5
87         }
88         node1.ShowInfo(); //NO.6
89         Console.Read();
90     }
91 }
View Code

代碼Code 5-6中我們可以通過一個節點訪問該節點以及該節點所有的后續節點(NO3、NO.4、NO.5以及NO.6處),之所以能夠這樣,是因為每個節點中都保存有下一個節點的引用(Next引用)。代碼中的node1. node2以及node3組成的單向鏈表在堆中的存儲結構如下圖5-5:

    圖5-5 單向鏈表在堆中的結構

通過一個單向鏈表中的節點對象,我們能夠訪問附加到它后面的所有其它節點,委托對象也能夠管理和訪問附加到它上面的其它委托,也能管理一個"鏈表",那么,我們是否可以按照單向鏈表的結構去理解委托的內部結構呢?答案雖是肯定的,但是委托內部的"鏈表"結構跟單向鏈表的實現原理卻不相同,它並不是通過Next引用與后續委托建立關聯,而是將所有委托存放在一個數組中,類似如下圖5-6:

    注:准確來講,委托內部結構不應該稱為"鏈表"。

圖5-6 委托結構

圖5-6中顯示委托內部不僅僅有Target和Method成員,還有一個數組成員,用來存儲附加到該委托對象中的其它委托。委托鏈在堆中的結構如下圖5-7:

圖5-7 委托鏈表在堆中的結構

圖5-7中顯示delegate1中包含delegate2. delegate3以及delegate4的引用,注意delegate2. delegate3以及delegate4中的數組列表不可能再包含有其它的委托引用,也就是說包含關系最多只有兩層,具體原因請參見下一小節有關委托的"不可改變"特性。

注:每一個委托類型都有一個公開的GetInvocationList()的方法,可以返回已附加到委托對象上的所有委托,也就是圖5-6中數組列表。另外,我們平時不區分委托對象和委托鏈表,提到委托對象,它很有可能就表示一個委托鏈表,這跟單向鏈表只包含一個節點時道理類似。

既然現在委托可以調用多個方法,那么它的Invoke方法內部是怎樣實現的呢?假如是一個簡單的單委托,Invoke()方法內部直接調用Method.Invoke方法,但如果包含其它委托,那么它就需要遍歷整個數組列表。代碼類似如下(假設委托的簽名為:返回值為null,含一個int類型參數):

 1 //Code 5-7
 2 public void Invoke(int a)
 3 {
 4     Delegate[] ds = GetInvocationList(); //get all delegates in array
 5     if(ds!=null) //contain a delegate chain
 6     {
 7         foreach(Delegate d in ds) // call each delegate
 8         {
 9             DelegateName dn = d as DelegateName;
10             dn(a);
11         }
12     }
13     else //don't contain a delegate chain
14     {
15         Method.Invoke(Target,new Object[]{a}); //call the Method on Target with argument 'a'
16     }
17 }

代碼Code 5-7中委托的Invoke方法先判斷該委托中是否包含其它委托,如果是,依次遍歷列表調用這些委托;否則,說明當前委托是一個單委托,直接調用Method.Invoke()方法。

5.1.3 委托的"不可改變"特性

所謂"不可改變"(Immutable),就是指一個對象創建之后,它的內容不能再改變。比如常見的String類型,我們創建的一個String對象之后,之后在該對象上的所有操作都不會影響對象原來的值,

 1 //Code 5-8
 2 Class Program
 3 {
 4     static void Main()
 5     {
 6         string a = "test"; // equal String a = new String("test");
 7         a.ToUpper(); //NO.1
 8         Console.WriteLine("a is " + a);
 9         // print out:
10         // a is test
11     }
12 }

代碼Code 5-8中a的值並沒有因為調用了a.ToUpper()方法而改變,如果想要讓a字符串都變為大寫格式,必須使用"a = a.ToUpper();"這樣的代碼,a.ToUpper()方法會返回一個全新的String對象,a重新指向該新對象。注意這里的"不可改變"指的是對象實例,而不是對象引用,也就是說我們還是可以將a指向其它對象。如下圖5-8:

圖5-8 String類型的不可變性

委托跟String類型一樣,也是不可改變的。換句話說,一旦委托對象創建完成后,這個對象就不能再被更改,那么我們前面講到的將一個委托附加到另外一個委托對象上形成一個委托鏈表又是怎么做到的呢?其實這個跟String.ToUpper()過程類似,我們對委托進行附加、移除等操作都會產生一個全新的委托,這些操作並不會改變原有委托對象。

 1 //Code 5-9
 2 EventHandler eh = new EventHandler(Fun1); //NO.1
 3 EventHandler tmp = eh; //tmp and eh point at the same delegate NO.2
 4 EventHandler eh2 = new EventHandler(Fun2); //NO.3
 5 eh += eh2; //NO.4
 6 // equal eh = Delegate.Combine(eh, eh2) as EventHandler;
 7 EventHandler tmp2 = eh; //tmp2 and eh point at the same delegate //NO.5
 8 EventHandler eh3 = new EventHandler(Fun3); //NO.6
 9 eh += eh3; //NO.7
10 //equal eh = Delegate.Combine(eh,eh3) as EventHandler;

上面代碼Code 5-9最終會在堆中產生5個委托對象,NO.1處創建第一個,讓eh指向它,NO.2處讓tmp與eh指向同一個委托,NO.3處創建第二個,讓eh2指向它,NO.4處合並了eh和eh2,但並沒有改變原來的eh和eh2,而是新創建了第三個,並且讓eh重新指向了新創建的第三個,NO.5處讓tmp2與eh指向同一個委托,NO.6處創建第四個,讓eh3指向它,NO.7處合並了eh和eh3,但並沒有改變原來的eh和eh3,而是新創建了第五個,並且讓eh重新指向了新創建的第五個。

我們對委托進行的每一個附加(+=或者Delegate.Combine)操作,都會創建一個全新的委托,該新創建委托的數組列表中包含原來兩個委托數組列表內容的總和,這個過程並不會影響原來的委托,移除(-=或者Delegate.Remove)操作類似。附加或移除委托過程,見下圖5-9:

圖5-9 附加或移除委托過程

圖5-9中D1、D2、D3、D4、D5、D6以及c、d、e均為委托對象引用。Delegate.Combine(D1,D2)產生了D3,D1並沒改變;Delegate.Combine(D3,D4)產生了D5,D5包含D3和D4中的數組列表內容之和,D3並沒有改變;Delegate.Remove(D5,D1)產生了D6,D5並沒有改變。由圖5-9可以看出,委托包含關系最多只有兩層,數組列表中的委托都屬於單委托,單委托不再包含其它委托。

注:文中的委托對象、單委托、委托鏈表都是指一個委托類型的對象。

5.1.4 委托的作用

委托是一種數據結構,專門用來管理和組織方法,並負責調用這些方法。那么為什么需要委托來調用方法呢?原因有以下三點:

(1)編程中無時無刻都存在着"方法調用",委托可以更方便更有組織的管理我們需要調用的方法,理論上沒有數量限制,只要是符合某一個委托簽名的方法都可以由該委托管理。我們可以使用委托一次性(有先后順序)地調用這些方法。在使用委托之前,我們調用方法是這樣:

圖5-10 不使用委托調用方法

圖5-10中為不使用委托直接調用方法的過程,我們每次只能調用一個方法。使用委托之后,我們可以調用一系列方法,如下圖5-11:

圖5-11 使用委托調用方法

上圖5-11為使用委托調用方法的過程,使用一個委托我們可以管理多個方法,並且一次性調用這些方法。能夠統一管理和組織被調用的方法,在編程中起到一個非常重要的作用,如后面講到的"事件編程"。

(2)使用普通方式調用方法只能是同步的(特殊方法除外),也就是說,被調用方法返回之前,調用線程一直處於等待狀態。使用委托調用方法時,有兩種方式可供選擇,既可以同步調用也可以異步調用,前者和普通調用方式一樣,而后者遵循"異步編程模型"的規律:方法的調用不會阻塞調用線程。

注:委托的異步調用關鍵在於它的BeginInvoke方法,該方法是Invoke方法的異步版本,詳見第六章關於異步編程的介紹。

(3)有了委托,方法可以作為一種參數在代碼中進行傳遞,這個類似於C++中的函數指針。委托的這種功能在框架中是非常有用的,框架一般由專業技術團隊編寫開發,由於框架的開發者並不知道框架使用者的具體代碼,那么框架又是怎樣調用使用者編寫的代碼呢?

框架有兩種方式調用框架使用者編寫的代碼,一種便是面向抽象編程。框架中盡量不出現某個具體類型的引用,而是使用抽象化的基類引用或者接口引用代替。只要框架使用者編寫的類型派生自抽象化的基類或實現了接口,框架均可以正確地調用它們。我們常見的使用using代碼塊來釋放對象非托管資源就是一個例子:

1 //Code 5-10
2 using(FileStream fs = new FileStream(…))
3 {
4     //use fs
5 }

代碼Code 5-10中要求FileStream類必須實現了IDisposable接口(事實上確實如此)。代碼Code 5-10經過編譯后,與下面代碼Code 5-11類似:

 1 //Code 5-11
 2 IDisposable dispose_target = new FileStream(…);
 3 try
 4 {
 5     //use filestream
 6 }
 7 finally
 8 {
 9     dispose_target.Dispose();
10 }

如上代碼Code 5-11所示,無論何時,FileStream對象都能正確地釋放非托管資源。框架認為所有使用using來釋放非托管資源的類型都已實現了IDisposable接口,因為只有這樣,它才能夠提前編寫釋放非托管資源的代碼(如finally中的dispose_target.Dispose())。沒有實現IDisposable接口的類型不能使用using關鍵字來釋放非托管資源。

    注:關於框架調用框架使用者代碼的過程,可以參見第二章中關於對"協議"的介紹,如圖2-14

框架調用框架使用者代碼的另外一種方式就是使用委托,將委托作為參數(變量)傳遞給框架,框架通過委托調用方法。異步編程中的一些方法往往帶有委托類型的參數,比如FileStream.BeginRead、Socket.BeginReceive等等(后續章節有講到)。這些方法都會帶有一個AsyncCallBack委托類型的參數,我們在使用這些方法時,如果給它傳遞一個委托對象,當異步操作執行完畢后,框架自動會調用我們傳遞給它的委托。還有下一節中講到的"事件",框架可以通過事件來調用框架使用者編寫的代碼,如事件發布者激發事件,調用事件注冊者的事件處理程序。

    注:我們使用.NET中預定義的一些類型、方法均可以當作框架中的一部分。

5.2 事件與委托的關系

委托的附加、移除以及調用,是沒有范圍限制的。如果一個類型包含一個委托成員,那么在類外部既可以給它附加或者移除委托,還可以調用這個委托。如下面代碼:

 1 //Code 5-12
 2 public delegate void DelegateName(int a,int b); //define a delegate type
 3 class A
 4 {
 5     public DelegateName MyDelegate; //define a delegate member
 6     Public A()
 7     {
 8         //
 9     }
10     public void DoSomething()
11     {
12         //
13         if(MyDelegate != null)
14         {
15             //… if something happen or if something is OK
16             int arg1 = 1; int arg2 = 2;
17             MyDelegate(arg1,arg2); //then call the delegate
18         }
19     }
20     //
21 }
22 class Program
23 {
24     static void Fun1(int a,int b)
25     {
26         Console.WriteLine("the result is " + (a + b).ToString());
27     }
28     static void Main()
29     {
30         A a = new A();
31         a.MyDelegate += new DelegateName(Fun1); //NO.1
32         a.DoSomething(); //NO.2
33         a.MyDelegate(1,2); //NO.3
34     }
35 }

代碼Code 5-12中,我們給a對象的MyDelegate附加一個方法后(NO.1處),a對象內部可以調用這個委托(NO.2處),a對象外部也可以調用這個委托(NO.3處)。也就是說,對MyDelegate委托成員的訪問是沒有限制的,從某種意義上講,這違背了"面向對象"思想,因為類里面的有些功能不應該對外公開,比如這里的"委托調用",該操作應該只能發生在類型內部。如果我們把MyDelegate定義為private私有變量,那么我們在類外部就不能給它附加和移除方法,為了解決這個問題,.NET中提出了一種介於public和private之間的另外一種訪問級別:在定義委托成員的時候給出event關鍵字進行修飾,前面加了event關鍵字修飾的public委托成員,只能在類外部進行附加和移除操作,而調用操作只能發生在類型內部。如果把代碼Code 5-12中A類聲明MyDelegate成員的代碼改為:

1 //Code 5-13
2 public event DelegateName MyDelegate;

按照Code 5-13中的方式定義的委托只能在A類內部調用,之前代碼Code 5-12中的NO.3處編譯通不過。

我們把類中設置了event關鍵字的委托叫作"事件","事件"本質上就是委托對象。事件的出現,限制了委托調用只能發生在一個類型的內部,如下圖5-12:

圖5-12 事件在程序調用中的位置

圖5-12中server中的委托使用了event關鍵字修飾,只能在server內部調用,外部只能進行附加和移除方法操作。當符合某一條件時,server內部會調用委托,這個時間不由我們(Client)控制,而是由系統(Server)決定。因此大部分時候,事件在程序中起到了回調作用(關於調用與回調的區別,參見第二章)。

調用加了event關鍵字修飾的委托也稱為"激發事件",調用方(圖5-12中的server)稱為"事件發布者",被調用方(圖5-12中的client)稱為"事件注冊者"(或"事件觀察者"、"事件訂閱者"等,本書中統一稱之為"事件注冊者"),附加委托的過程稱之為"注冊事件"(或"綁定事件"、"監聽事件"、"訂閱事件"等,本書中統一稱之為"注冊事件"),移除委托的過程稱之為"注銷事件"。通過委托調用的方法稱為"事件處理程序"。

注:將只能在類型內部調用的委托稱之為"事件",主要是因為這些委托一般是當server中發生某件事件(或符合某個條件)時才被server調用。我們所熟知的Button.ClickTextBox.TextChangedForm.FormClosing等事件,都屬於這種情況。

    "事件"在.NET中起到了重要作用,它為框架與框架使用者編寫代碼之間的交互做出了重大貢獻。

5.3 使用事件編程

5.3.1 注銷跟注冊事件同樣重要

前面在講到委托結構組成的時候就知道,委托內部包含了要調用的方法(Method成員),以及該方法所屬的對象(Target成員)。當我們注冊事件時,其實就是附加委托的過程,將一個新委托附加到委托鏈表中。事件注冊者向事件發布者注冊事件后,發布者就會保存一個注冊者的引用(委托中的Target成員),發布者激發事件,其實就是通過該引用調用注冊者的事件處理程序。當我們注銷事件時,其實就是移除發布者對注冊者的引用。

第四章講到,堆中的對象實例如果存在引用指向它,那么CLR就不會回收它在堆中占用的內存,哪怕這個對象已經沒有使用價值。注冊事件使一個新的引用指向了事件注冊者,如果我們不及時注銷事件,那么這個引用將會一直存在。

5.3.2 多線程中使用事件

在通常編程中,我們激發一個事件之前需要先判斷該事件是否為空,如果不為空,我們就可以激發該事件(調用委托),類似代碼如下:

1 //Code 5-14
2 public event MyDelegate SomeEvent;
3 if(SomeEvent != null) //NO.1
4 {
5     //do something
6     SomeEvent(arg1,arg2); //NO.2 call the delegate
7 }

代碼Code 5-14中NO.1處先檢查SomeEvent是否為空,如果為空,說明沒有人注冊過該事件,就不會執行if塊中的語句;如果不為空,說明已經有人注冊過該事件,就執行if塊中的語句,調用委托(圖中NO.2處)。在單線程中,上面代碼沒有任何問題,但是如果在多線程中,以上代碼就有可能拋出異常:如果在NO.1處if判斷為true,在NO.2執行之前,其它線程將SomeEvent改變為null,這時候再回頭執行NO.2時,就會拋出NullReferenceException的異常。

注:本章前面講到的"委托不可改變特性"指的是委托實例不可改變,類似String類型,委托引用仍然可以改變,所以SomeEvent可以指向其它實例,甚至指向null

為了解決多線程中事件編程容易引發的異常,我們需要利用"委托不可改變"這一特點。由於我們對一個委托的任何操作都不會改變該委托本身,只會產生新的委托,那么我們完全可以在if判斷語句之前,使用一個局部臨時變量來指向委托實例,之后所有的操作都針對該局部臨時變量。由於局部變量不可能被其它人修改,所以它永遠都不會指向null。

1 //Code 5-15
2 MyDelegate tmp = SomeEvent;
3 if(tmp != null) //NO.1
4 {
5     //do something
6     tmp(arg1,arg2); //NO.2
7 }

上述代碼Code 5-15中,先讓tmp和SomeEvent指向同一委托實例,NO.1處if判斷為true,if塊中的tmp在任何時候都不會被其它線程修改為null,因為其它線程只能修改SomeEvent,並且我們對SomeEvent的任何操作都不會改變它所指向的委托實例。這種解決方法其實跟我們在做一個除法運算時檢測除數是否為零的原理一樣,如果在多線程中,我們檢查完除數不為零后,直接進行除法運算,有可能拋出異常,如下代碼:

 1 //Code 5-16
 2 class A
 3 {
 4     //
 5     public int x;
 6     public A()
 7     {
 8         //
 9     }
10     public int DoSomething(int y)
11     {
12         if(x != 0) //NO.1
13         {
14             return y/x; //NO.2
15         }
16         else
17         {
18             return 0;
19         }
20     }
21 }

上述代碼Code 5-16中,如果NO.1處if判斷為true后,在NO.2執行之前x的值被其它線程改變為0,那么代碼執行到NO.2處時就會拋出異常。正確的做法是,使用一個臨時變量存儲x的值,之后所有的操作都是針對該臨時變量。Code 5-16中類A的DoSomething方法可以修改為:

 1 //Code 5-17
 2 public int DoSomething(int y)
 3 {
 4     int tmp = x;
 5     if(tmp != 0) //NO.1
 6     {
 7         return y/tmp; //NO.2
 8     }
 9     else
10     {
11         return 0;
12     }
13 }

上述代碼Code 5-17中,NO.1處if判斷為true后,tmp的值就永遠不會為零,其它線程對x的所有操作都不會影響到tmp的值,因此NO.2處不可能再有異常拋出。這個原理跟我們剛學習編程的時候碰到的形參和實參的關系一樣,在值傳遞過程中,形參和實參是相互獨立的,形參改變不會影響到實參。

注:.NET中值類型賦值都是值傳遞,也就是說賦值后會產生一個一模一樣的拷貝,兩者之間是相互獨立互不影響的。引用類型賦值也是值傳遞,因為它傳遞的是對象引用,賦值后兩個引用指向堆中同一個實例,關於值類型與引用類型賦值請參見第三章。

5.3.3 委托鏈表的分步調用

調用任何方法都有可能出現異常,因此,通過委托調用方法時,我們最好把調用代碼放在try/catch塊中,類似如下:

 1 //Code 5-18
 2 class A
 3 {
 4     //
 5     public event MyDelegate SomeEvent;
 6     public A()
 7     {
 8         //
 9     }
10     public void DoSomething()
11     {
12         //
13         MyDelegate tmp = SomeEvent; //NO.1
14         if(tmp != null)
15         {
16             //
17             try //NO.2
18             {
19                 tmp(arg1,arg2); //NO.3
20             }
21             catch
22             {
23                 
24             }
25         }
26     }
27 }

上述代碼Code 5-18中,激發事件的代碼(NO.3處)放在了try/catch塊中,這樣以來,萬一事件注冊者中的事件處理程序拋出了沒有被處理的異常,try/catch便會捕獲該異常,程序不會異常終止。

調用委托鏈時,如果某一個委托對應的方法拋出了異常,那么剩下的其它委托將不再調用。這個很容易理解,本來是按先后順序依次調用方法,如果其中某一個拋出異常,剩下的肯定被跳過。為了解決這個問題,單單是將激發事件的代碼放在try/catch塊中是不夠的,我們需要分步調用每個委托,將每一步的調用代碼均放在try/catch塊中。類A的DoSomething方法修改為:

 1 //Code 5-19
 2 public void DoSomething()
 3 {
 4     //
 5     MyDelegate tmp = SomeEvent; //NO.1
 6     if(tmp != null)
 7     {
 8         //
 9         Delegate[] delegates = tmp.GetInvocationList(); //NO.2
10         foreach(Delegate d in delegates)
11         {
12             MyDelegate del = d as MyDelegate;
13             try //NO.3
14             {
15                 del(arg1,arg2); //NO.4
16             }
17             catch
18             {
19                 
20             }
21         }
22     }
23 }

上述代碼Code 5-19中,我們沒有直接使用tmp來調用委托鏈表,而是先通過tmp.GetInvocationList方法來獲取委托鏈表中的委托集合(NO.2處),然后再使用foreach循環遍歷集合,分步調用每個委托(NO.4處),分步調用過程均放在了try/catch塊中,這樣一來,任何一個方法拋出異常都不會影響到其它委托的調用。

注:在單線程中使用事件時,激發事件之前不需要使用一個臨時委托變量,本小節所有代碼為了與前一小節一致,都使用了臨時委托。現實編程中,要看我們定義的類型是否在多線程環境中使用。Winform編程中的Control類(及其派生類)在設計之初就只讓它們運行在UI線程中,因此它們激發事件時,都沒有考慮多線程的情況。

5.3.4 正確定義一個使用了事件的類

前面說到過,.NET中的"事件"在框架與客戶端代碼交互過程中起到了關鍵作用。那么平常開發過程中,應該怎樣去定義一個使用了事件的類型,既能夠讓該類型的使用者更容易地去使用它,也能夠讓該類型的開發者更方便地去維護它呢?其實定義一個使用了事件的類型有一套標准方法。下面從命名、激發事件以及組織事件三個方面詳細說明:

(1)命名;

前面講到過,通常情況下,當某件事情發生時,對象內部就會激發事件,通知事件注冊者,調用對應的事件處理程序,因此代碼中事件的命名最好跟這個發生的事情有關系。比如有一個負責收發Email的類,當接收到新的郵件時,應該會激發一個類似叫"NewEmailReceived"的事件,去通知注冊了這個事件的其他人,我們最好不要將這個事件定義為"NewEmailReceive"。除了事件本身的命名,事件所屬委托類型的命名也同樣有標准格式,一般以"事件名+EventHandler"這種格式來給委托命名,前面提到的NewEmailReceived事件對應的委托類型名稱應該是"NewEmailReceivedEventHandler"。激發事件時會傳遞一些參數,這些參數一般繼承自EventArgs類型(后者為.NET框架預定義類型),以"事件名+EventArgs"來命名,比如前面提到的NewEmailReceived事件在激發時傳遞的參數類型名稱應該是"NewEmailReceivedEventArgs"。下面為示例代碼:

 1 //Code 5-20
 2 private delegate void NewEmailReceivedEventHandler(object sender,NewEmailReceivedEventArgs e); //define a delegate NO.1
 3 class EmailManager
 4 {
 5     //
 6     public event NewEmailReceivedEventHandler NewEmailReceived; //define e event member NO.2
 7     public EmailManager()
 8     {
 9         //
10     }
11 }
12 class NewEmailReceivedEventArgs:EventArgs //define event argument class derived from EventArgs NO.3
13 {
14     //
15     public NewEmailReceivedEventArgs()
16     {
17         //
18     }
19 }

上述代碼Code 5-20中NO.1處定義一個委托,NO.2處使用該委托定義一個事件,NO.3處定義一個事件參數類,它派生自EventArgs類(通常情況下,EventArgs為所有事件參數類的基類,如果激發一個事件不帶任何參數,那么可以直接使用EventArgs)。

注:事件的委托簽名一般包含兩個參數,一個object類型,表示事件發布者(自己),一個為從EventArgs派生出來的子類型,包含激發事件時所帶的參數。

(2)激發事件;

當一個類內部發生某件事情(或者說某個條件成立時),類內部就會激發事件,通知事件的所有注冊者。為了便於類型的使用者能夠擴展這個類型,比如改變激發事件的邏輯,我們通常使用虛方法去激發事件,比如前面說到的郵件類EmailManager中激發NewEmailReceived事件應該是這樣編寫代碼:

 1 //Code 5-21
 2 private delegate void NewEmailReceivedEventHandler(object sender,NewEmailReceivedEventArgs e); //define a delegate NO.1
 3 class EmailManager
 4 {
 5     //
 6     public event NewEmailReceivedEventHandler NewEmailReceived; //define e event member NO.2
 7     public EmailManager()
 8     {
 9         //
10     }
11     private void DoSomething()
12     {
13         //
14         if(/**/) //NO.4
15         {
16             NewEmailReceivedEventArgs e = new NewEmailReceivedEventArgs();
17             OnNewEmailReceived(e); //NO.5
18         }
19     }
20     protected void virtual OnNewEmailReceived(NewEmailReceivedEventArgs e) //NO.6
21     {
22         if(NewEmailReceived != null)
23         {
24             NewEmailReceived(this,e); //NO.7
25         }
26     }
27 }
28 class NewEmailReceivedEventArgs:EventArgs //define event argument class derived from EventArgs NO.3
29 {
30     //
31     public NewEmailReceivedEventArgs()
32     {
33         //
34     }
35 }

上述代碼Code 5-21中,NO.1、NO.2以及NO.3處含義與之前解釋相同,NO.4處當類中某個條件成立時,並沒有馬上激發事件,而是調用了預先定義的一個虛方法OnNewEmailReceived(NO.6處),在該虛方法內部激發事件(NO.7處),之所以要把激發事件的代碼放在一個單獨的虛方法中,這是為了讓從該類型(EmailManager)派生出來的子類能夠重寫虛方法,從而改變激發事件的邏輯。下面代碼Code 5-22定義一個EmailManager的派生類EmailManagerEx:

 1 //Code 5-22
 2 class EmailManagerEx:EmailManager
 3 {
 4     //
 5     protected override void OnNewEmailReceived(NewEmailReceivedEventArgs e)
 6     {
 7         //…do something here
 8         if(/**/) //NO.1
 9         {
10             base.OnNewEmailReceived(e); //NO.2
11         }
12         else
13         {
14             // NO.3
15         }
16     }
17 }

如上代碼Code 5-22所述,派生類中重寫OnNewEmailReceived虛方法后,可以重新定義激發事件的邏輯。如果NO.1處if判斷為true,則正常激發事件(NO.2處);否則,不激發事件(NO.3處)。我們能夠在派生類EmailManagerEx的OnNewEmailReceived虛方法中做許許多多其它的事情,包括示例代碼中"取消激發事件"。

虛方法的命名一般為"On+事件名",另外該虛方法必須定義為protected,因為派生類中很可能要調用基類的虛方法。

(3)組織事件。

事件類似屬性,僅僅只是類型對外公開的一個中介,通過它可以訪問類型內部的數據。換句話說,無論事件還是屬性,真正存儲數據的成員並沒有對外公開,比如屬性基本都對應有相應的私有字段,每個事件也對應有相應的私有委托成員。我們通過event關鍵字聲明的公開事件,經過編譯器編譯之后,生成的代碼類似如下:

 1 //Code 5-23
 2 class EmailManager
 3 {
 4     //
 5     private NewEmailReceivedEventHandler _newEmailReceived; //NO.1
 6     public event NewEmailReceivedEventHandler NewEmailReceived
 7     {
 8         [MethodImpl(MethodImplOptions.Synchronized)] //NO.2
 9         add //NO.3
10         {
11             _newEmailReceived = Delegate.Combine(_newEmailReceived,value) as NewEmailReceivedEventHandler;
12         }
13         [MethodImpl(MethodImplOptions.Synchronized)]
14         remove //NO.4
15         {
16             _newEmailReceived = Delegate.Remove(_newEmailReceived,value) as NewEmailReceivedEventHandler;
17         }
18     }
19 }

如上代碼Code 5-23所示,編譯器編譯之后,將一個事件分成了兩部分,一個私有委托變量_newEmailReceived(NO.1處)和一個事件訪問器add/remove(NO3和NO.4處),前者類似一個字段,后者類似屬性訪問器set/get。可以看出,真正存儲事件數據的是私有委托成員_newEmailReceived。

注:代碼Code 5-23NO.2[MethodImpl(MethodImplOptions.Synchronized)]的作用類似lock(this);,為了解決多線程中訪問同步問題,這個是官方給出的默認方法,該方法存在缺陷,因為使用lock加鎖時,鎖對象不應該是對外公開的,this顯然是對外公開的,很有可能出現對this重復加鎖的情況,從而造成死鎖。我們可以自己實現事件訪問器add/remove,在其中添加自己的lock塊,從而避免使用默認的lock(this)

下圖5-13為一個類中屬性和事件的作用:

圖5-13 屬性和事件的作用

有些類型包含的事件非常多,比如.NET3.5中System.Windows.Forms.Control就包含有69個公開事件。一個Control類(或其派生類)對象編譯后,對象內部就會產生幾十個類似代碼Code 5-23中_newEmailReceived這樣的私有委托成員,這無疑會增加內存消耗,為了解決這個問題,我們一般需要自己定義事件訪問器add/remove,並且自己定義數據結構去存儲組織事件數據,不再使用編譯器默認生成的私有委托成員。微軟在.NET中的標准做法是:定義一個類似Dictionary功能的容器類型EventHandlerList,專門用來存放委托。一個類型自定義事件訪問器add/remove后的代碼類似如下:

 1 //Code 5-24
 2 class EmailManager
 3 {
 4     private static readonly object _newEmailReceived; //NO.1
 5     private EventHandlerList _handlers = new EventHandlerList(); //NO.2
 6     public event NewEmailReceivedEventHandler NewEmailReceived
 7     {
 8         add
 9         {
10             _handlers.AddHandler(_newEmailReceived,value); //NO.3
11         }
12         remove
13         {
14             _handlers.RemoveHandler(_newEmailReceived,value); //NO.4
15         }
16     }
17     protected virtual void OnNewEmailReceived(NewEmailReceivedEventArgs e)
18     {
19         NewEmailReceivedEventHandler newEmailReceived = _handlers[_newEmailReceived] as NewEmailReceivedEventHandler; //NO.5
20         if(newEmailReceived != null)
21         {
22             newEmailReceived(this,e);
23         }
24     }
25 }

如上代碼Code 5-24所述,自定義事件訪問器add/remove后,使用EventHandlerList來存儲事件數據,編譯器不再生成默認的私有委托成員,所有的事件數據均存放在_handlers容器中(NO.2處),NO.1處定義了訪問容器的key,NO.3以及NO.4處訪問容器,NO.5處在激發事件之前,先判斷容器_handlers中是否有人注冊了該事件。

注:自己定義事件訪問器還有其它很多作用,比如自己實現線程同步鎖、給事件標注[NonSerializable]屬性(編譯器生成的私有委托成員默認都是Serializable)等。

上面提到的命名規范、激發事件以及組織事件的方式,這三個是微軟給出官方代碼中的標准,所有官方源碼資料中都遵守了這三個規范。我們平時開發過程中,也應該遵守這些原則,編寫出更高質量的代碼。

5.4 弱委托

5.4.1 強引用與弱引用

前面章節提到過,一個引用類型對象包括"引用"和"實例"兩部分。如果堆中實例至少有一個引用指向它(不管該引用存在於棧中還是堆中),CLR就不能對其進行內存回收,同時我們一定能夠通過引用訪問到堆中實例。換句話說,引用與實例是一種"強關聯"關系,我們稱這種引用為"強引用"(Strong Reference),堆中對象實例能否被訪問完全掌握在程序手中。

圖5-14 強引用

圖5-14中a是A的強引用,b是B的強引用,B中又存在一個C的強引用,只要棧中a和b存在,堆中A、B以及C就會一直存在。我們平時編程過程中使用new關鍵字創建一個對象時返回的引用便是強引用,比如"A a=new A();"中,a就是強引用。

強引用的優點是程序中只要有強引用的存在,就一定能夠訪問到堆中的對象實例。由於只要有一個強引用存在,CLR就不會回收堆中的對象實例,這就會出現一個問題:如果我們程序中沒有合理地管理好強引用,在該移除強引用的時候沒有移除它們,這便會導致堆中的對象實例大量累積,時間一長,就會出現內存不足的情況,尤其當這些對象占用內存比較大的時候。管理好強引用並不是一件容易的事情,通常情況下,強引用在程序運行過程中不斷的傳遞,到最后有些幾乎發現不了它們的存在。雖然有時候開發者認為對象已經使用完畢,但是程序中還是會保存這些對象的強引用直到很長一段時間,甚至會一直到程序運行結束。在事件編程中,委托的Target成員,就是對事件注冊者的強引用,如果事件注冊者沒有注銷事件,這個Target強引用便會一直存在,堆中的事件注冊者內存就一直不會被CLR回收,這對開發人員來講,幾乎是很難發覺的。

注:像"A a = new A();"中的a稱為"顯式強引用(Explicit Strong Reference)",類似委托中包含的不明顯的強引用,我們稱之為"隱式強引用(Implicit Strong Reference)"。

對於"強引用",有一個概念與之對應,即"弱引用"。弱引用與對象實例之間屬於一種"弱關聯"關系,跟強引用與對象實例的關系不一樣,就算程序中有弱引用指向堆中對象實例,CLR還是會把該對象實例當做回收目標。程序中使用弱引用訪問對象實例之前必須先檢查CLR有沒有回收該對象內存。換句話說,當堆中一個對象實例只有弱引用指向它時,CLR可以回收它的內存。使用弱引用,堆中對象能否被訪問同時掌握在程序和CLR手中。

圖5-15 弱引用

圖5-15中a是A的弱引用,b是B的弱引用,B中又包含一個C的弱引用,不管a和b是否存在,堆中A、B以及C都有可能成為CLR的回收目標。

創建一個弱引用很簡單,使用WeakReference類型,給它的構造方法傳遞一個強引用作為參數,代碼如下:

 1 //Code 5-25
 2 class A
 3 {
 4     public A()
 5     {
 6         //
 7     }
 8     public void DoSomething()
 9     {
10         Console.WriteLine("I am OK");
11     }
12     //
13 }
14 class Program
15 {
16     static void Main()
17     {
18         A a = new A();
19         WeakReference wr = new WeakReference(a); //NO.1
20         a = null; //NO.2
21         //do something else
22         A tmp = wr.Target; //NO.3
23         if(wr.IsAlive) //NO.4
24         {
25             tmp.DoSomething(); //NO.5
26             tmp = null;
27         }
28         else
29         {
30             Console.WriteLine("A is dead");
31         }
32     }
33 }

代碼Code 5-25中創建了一個A對象的弱引用(NO.1處),然后馬上將它的臨時強引用a指向null(NO.2處),此時只有一個弱引用指向A對象。程序運行一段時間后(代碼中do something處),當需要通過弱引用wr訪問A對象的時候,我們必須先檢查CLR有沒有回收它的內存(NO.4處),如果沒有,我們正常訪問A對象;否則,我們不能再訪問A對象。

在編程過程中,我們很難管理好強引用,從而造成不必要的內存開銷。尤其前面講到的"隱式強引用",在使用過程中不易發覺它們的存在。使用弱引用,CLR回收堆中對象內存不再根據程序中是否有弱引用指向它,因此程序中有沒有多余的弱引用指向某個對象對CLR回收該對象內存沒有任何影響。弱引用特別適合用於那些對程序依賴程度不高的對象,也就是那些對象生命期不是主要由程序控制的對象。比如事件編程中,事件發布者對事件注冊者的存在與否不是很關心,如果注冊者在,那就激發事件並通知注冊者;如果注冊者已經被CLR回收內存,那么就不通知它,這完全不會影響程序的運行。

5.4.2 弱委托定義

前面講到過,委托包含兩個部分:一個Object類型Target成員,代表被調用方法的所有者,如果方法為靜態方法,Target為null;另一個是MethodInfo類型的Method成員,代表被調用方法。由於Target成員是一個強引用,所以只要委托存在,那么方法的所有者就會一直在堆中存在而不能被CLR回收。如果我們將委托中的Target強引用換成弱引用的話,那么不管委托存在與否,都不會影響方法的所有者在堆中內存的回收。這樣一來,我們在使用委托調用方法之前需要先判斷方法的所有者是否已經被CLR回收。我們稱將Target成員換成弱引用之后的委托為"弱委托",弱委托定義如下:

 1 //Code 5-26
 2 class WeakDelegate
 3 {
 4     WeakReference _weakRef; //NO.1
 5     MethodInfo _method; //NO.2
 6     public WeakDelegate(Delegate d)
 7     {
 8         _weakRef = new WeakReference(d.Target);
 9         _methodInfo = d.Method;
10     }
11     public object Invoke(param object[] args)
12     {
13         object obj = _weakRef.Target;
14         if(_weakRef.IsAlive) //NO.3
15         {
16             return _method.Invoke(obj,args); //NO.4
17         }
18         else
19         {
20             return null;
21         }
22     }
23 }

如上代碼Code 5-26所示,我們定義了一個WeakDelegate弱委托類型,它包含一個WeakReference類型_weakRef成員(NO.1處),它是一個弱引用,指向被調用方法的所有者,還包含一個MethodInfo類型_method成員(NO.2處),它表示委托要調用的方法。我們在弱委托的Invoke成員方法中,先判斷被調用方法的所有者是否還在堆中(NO.3處),如果在,我們調用方法,否則返回null。

弱委托將委托與被調用方法的所有者之間的關系由"強關聯"轉換成了"弱關聯",方法的所有者在堆中的生命期不再受委托的控制,下圖5-16顯示弱委托的結構:

圖5-16 弱委托結構

如上圖5-16所示,圖中上部分表示一個普通委托的結構,下部分表示一個弱委托的結構,虛線框表示弱引用,堆中實例的內存不再受該弱引用影響。

注:本小節示例代碼中的WeakDelegate類型並沒有提供類似Delegate.Combine以及Delegate.Remove這樣操作委托鏈表的方法,當然也沒有弱委托鏈表的功能,這些功能可以仿照單向鏈表的結構去實現,把每個弱委托都當作鏈表中的一個節點。請參照5.1.2小節中講到的單向鏈表。

5.4.3 弱委托使用場合

我們在使用事件編程時,如果一個事件注冊者向事件發布者注冊了一個事件,那么發布者就會對注冊者保存一個強引用。如果事件注冊者未正確地注銷事件,那么發布者的委托鏈表中就一直包含一個對該注冊者的強引用,這樣一來,注冊者在堆中的內存永遠都不會被CLR回收,如果這樣的注冊者屬於大對象或者數目眾多,很輕易就會造成堆中內存不足。弱委托就恰好能夠解決這個問題,我們可以將事件編程中用到的委托替換為弱委托,那么事件發布者與事件注冊者的關系如下圖5-17:

圖5-17 弱委托在事件編程中的應用

如上圖5-17所示,事件發布者中不再保留對事件注冊者的強引用。當發布者激發事件時,先判斷注冊者是否存在(堆中內存是否被CLR回收),如果存在,就通知注冊者;否則將對應弱委托從鏈表中刪除。

注:弱委托鏈表請讀者自己去實現。

5.5 本章回顧

委托與事件幾乎出現在.NET編程的每一個地方,它們是.NET中最重要的知識點之一。程序的運行就是一個個調用與被調用的過程,而委托的主要作用就是"調用方法",它是銜接調用者與被調用者的橋梁。本章開頭介紹了.NET中委托的概念和組成結構,同時介紹了委托鏈表以及它的"不可改變"特性;之后介紹了委托與事件的關系,我們明白了事件是一種特殊的委托對象;緊接着講到了.NET中使用事件編程時需要關注的幾條注意事項,它們是在事件編程過程中常遇到的陷阱;章節最后還提到了"弱引用"和"弱委托"的概念以及它們的實現原理,"弱委托"是解決內存泄露的一種有效方法。

    本章提到了委托的三個作用:第一,它允許把方法作為參數,傳遞給其它的模塊;第二,它允許我們同時調用多個具有相同簽名的方法;第三,它允許我們異步調用任何方法。這三個作用奠定了委托在.NET編程中的絕對重要地位。

5.6 本章思考

1.簡述委托包含哪兩個重要部分。

A:委托包含兩個重要組成:Method和Target,分別代表委托要調用的方法和該方法所屬的對象(如果為靜態方法,則Target為null)。

2.怎樣簡單地說明委托的不可改變特性?

A:對委托的所有操作,均需要將操作后的結果在進行賦值,比如使用"+="、"-="將操作后的結果賦值給原委托變量。這說明對委托的操作均不能改變委托本身。

3."事件是委托對象"是否准確?

A:准確。.NET中的事件是一種特殊的委托對象,即在定義委托對象時,在聲明語句前增加了"event"關鍵字。事件的出現確保委托的調用只能發生在類型內部。

4.為什么說委托是.NET中的"重中之重"?

A:因為程序的運行過程就是方法的不斷調用過程,而委托的作用就是"調用方法",它不僅能夠將方法作為參數傳遞,還能同時(同步或異步)調用多個具有相同簽名的方法。

5.弱委托的關鍵是什么?

A:弱委托的關鍵是弱引用,弱委托是通過弱引用實現的。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM