編程之基礎:數據類型(一)html
編程之基礎:數據類型(二)node
不免的尷尬:代碼依賴數據結構
物以類聚:對象也有生命app
委託是.NET編程中的重點之一,委託的做用簡單歸納起來就是"調用方法"。使用委託,咱們能夠異步(同步)調用方法、一次調用多個方法甚至能夠將方法做爲參數傳遞給別人供別人回調。程序的運行過程即是方法之間的調用過程,因此委託是.NET開發者必須掌握的知識點之一。.NET編程中的事件創建在委託的基礎之上,要掌握事件的用法必須先了解委託。框架
委託的字面意思爲"把什麼什麼東西託付給某某人去作",偏向於一個動做,可是在.NET中,委託倒是一個名詞,表示"代理"或者"中間人"的意思。A原本要找B辦事,可是它沒有直接找B,而是託付給C,讓C去找B把事兒給辦了,若是按照"委託"字面意思去理解,"A找C"的這個行爲叫"委託",可是在.NET中,C這我的叫"委託"。異步
圖5-1 .NET中委託含義 ide
圖5-1中A表示請求辦事情的人(請求方),B是最終處理事情的人(應答方),C表示.NET中的委託。既然C是中間人,那麼它確定包含有B的一些信息,否則怎麼去找B辦事情?
注:本書中以後出現的全部與"委託"有關的詞彙均指.NET中的委託,也就是圖5-1中的C部分。另外,按照第二章中所講的內容,A能夠稱爲Client,B則稱爲Server。
委託的職責就是代替請求方去找應答方辦事情,在程序中,體現爲調用應答方的方法,換句話說,委託其實就是起到"調用方法"的做用。
程序中調用一個方法的必備條件是:知道要調用的方法,知道這個方法的全部者(若是該方法爲實例方法)。所以一個委託中至少要包含圖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)。
上一小節中提到的委託都是單委託,它只對一個方法進行封裝,也就是說,使用單委託只能調用一個方法。
以前提到過,一個委託應該能夠調用多個方法,只要這些方法的簽名與該委託一致,那麼怎樣讓一個委託同時調用兩個或者兩個以上的方法呢? 咱們代碼中很好實現,直接使用加法賦值運算符(+=)將多個方法附加到委託對象上,
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)能夠從委託對象上移除一個方法,不影響其它方法。
注:確切的說,應該是將委託附加到委託對象上,另外代碼中使用的都是靜態方法,這時候委託內部Target爲null。+=和-=運算符至關於Delegate類的靜態方法Delegate.Combine和Delegate.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 }
代碼Code 5-6中咱們能夠經過一個節點訪問該節點以及該節點全部的後續節點(NO三、NO.四、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()方法。
所謂"不可改變"(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中D一、D二、D三、D四、D五、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能夠看出,委託包含關係最多隻有兩層,數組列表中的委託都屬於單委託,單委託再也不包含其它委託。
注:文中的委託對象、單委託、委託鏈表都是指一個委託類型的對象。
委託是一種數據結構,專門用來管理和組織方法,並負責調用這些方法。那麼爲何須要委託來調用方法呢?緣由有如下三點:
(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中預約義的一些類型、方法都可以看成框架中的一部分。
委託的附加、移除以及調用,是沒有範圍限制的。若是一個類型包含一個委託成員,那麼在類外部既能夠給它附加或者移除委託,還能夠調用這個委託。以下面代碼:
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.Click、TextBox.TextChanged、Form.FormClosing等事件,都屬於這種狀況。
"事件"在.NET中起到了重要做用,它爲框架與框架使用者編寫代碼之間的交互作出了重大貢獻。
前面在講到委託結構組成的時候就知道,委託內部包含了要調用的方法(Method成員),以及該方法所屬的對象(Target成員)。當咱們註冊事件時,其實就是附加委託的過程,將一個新委託附加到委託鏈表中。事件註冊者向事件發佈者註冊事件後,發佈者就會保存一個註冊者的引用(委託中的Target成員),發佈者激發事件,其實就是經過該引用調用註冊者的事件處理程序。當咱們註銷事件時,其實就是移除發佈者對註冊者的引用。
第四章講到,堆中的對象實例若是存在引用指向它,那麼CLR就不會回收它在堆中佔用的內存,哪怕這個對象已經沒有使用價值。註冊事件使一個新的引用指向了事件註冊者,若是咱們不及時註銷事件,那麼這個引用將會一直存在。
在一般編程中,咱們激發一個事件以前須要先判斷該事件是否爲空,若是不爲空,咱們就能夠激發該事件(調用委託),相似代碼以下:
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中值類型賦值都是值傳遞,也就是說賦值後會產生一個如出一轍的拷貝,二者之間是相互獨立互不影響的。引用類型賦值也是值傳遞,由於它傳遞的是對象引用,賦值後兩個引用指向堆中同一個實例,關於值類型與引用類型賦值請參見第三章。
調用任何方法都有可能出現異常,所以,經過委託調用方法時,咱們最好把調用代碼放在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線程中,所以它們激發事件時,都沒有考慮多線程的狀況。
前面說到過,.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.一、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-23中NO.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)等。
上面提到的命名規範、激發事件以及組織事件的方式,這三個是微軟給出官方代碼中的標準,全部官方源碼資料中都遵照了這三個規範。咱們平時開發過程當中,也應該遵照這些原則,編寫出更高質量的代碼。
前面章節提到過,一個引用類型對象包括"引用"和"實例"兩部分。若是堆中實例至少有一個引用指向它(無論該引用存在於棧中仍是堆中),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回收內存,那麼就不通知它,這徹底不會影響程序的運行。
前面講到過,委託包含兩個部分:一個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小節中講到的單向鏈表。
咱們在使用事件編程時,若是一個事件註冊者向事件發佈者註冊了一個事件,那麼發佈者就會對註冊者保存一個強引用。若是事件註冊者未正確地註銷事件,那麼發佈者的委託鏈表中就一直包含一個對該註冊者的強引用,這樣一來,註冊者在堆中的內存永遠都不會被CLR回收,若是這樣的註冊者屬於大對象或者數目衆多,很輕易就會形成堆中內存不足。弱委託就剛好可以解決這個問題,咱們能夠將事件編程中用到的委託替換爲弱委託,那麼事件發佈者與事件註冊者的關係以下圖5-17:
圖5-17 弱委託在事件編程中的應用
如上圖5-17所示,事件發佈者中再也不保留對事件註冊者的強引用。當發佈者激發事件時,先判斷註冊者是否存在(堆中內存是否被CLR回收),若是存在,就通知註冊者;不然將對應弱委託從鏈表中刪除。
注:弱委託鏈表請讀者本身去實現。
委託與事件幾乎出如今.NET編程的每個地方,它們是.NET中最重要的知識點之一。程序的運行就是一個個調用與被調用的過程,而委託的主要做用就是"調用方法",它是銜接調用者與被調用者的橋樑。本章開頭介紹了.NET中委託的概念和組成結構,同時介紹了委託鏈表以及它的"不可改變"特性;以後介紹了委託與事件的關係,咱們明白了事件是一種特殊的委託對象;緊接着講到了.NET中使用事件編程時須要關注的幾條注意事項,它們是在事件編程過程當中常遇到的陷阱;章節最後還提到了"弱引用"和"弱委託"的概念以及它們的實現原理,"弱委託"是解決內存泄露的一種有效方法。
本章提到了委託的三個做用:第一,它容許把方法做爲參數,傳遞給其它的模塊;第二,它容許咱們同時調用多個具備相同簽名的方法;第三,它容許咱們異步調用任何方法。這三個做用奠基了委託在.NET編程中的絕對重要地位。
1.簡述委託包含哪兩個重要部分。
A:委託包含兩個重要組成:Method和Target,分別表明委託要調用的方法和該方法所屬的對象(若是爲靜態方法,則Target爲null)。
2.怎樣簡單地說明委託的不可改變特性?
A:對委託的全部操做,均須要將操做後的結果在進行賦值,好比使用"+="、"-="將操做後的結果賦值給原委託變量。這說明對委託的操做均不能改變委託自己。
3."事件是委託對象"是否準確?
A:準確。.NET中的事件是一種特殊的委託對象,即在定義委託對象時,在聲明語句前增長了"event"關鍵字。事件的出現確保委託的調用只能發生在類型內部。
4.爲何說委託是.NET中的"重中之重"?
A:由於程序的運行過程就是方法的不斷調用過程,而委託的做用就是"調用方法",它不只可以將方法做爲參數傳遞,還能同時(同步或異步)調用多個具備相同簽名的方法。
5.弱委託的關鍵是什麼?
A:弱委託的關鍵是弱引用,弱委託是經過弱引用實現的。