原文:https://www.codeproject.com/articles/85296/important-uses-of-delegates-and-events 原文作者: Shivprasad koirala 介紹 在這篇文章中, 我們會嘗試著去理解delegate能解決什麼樣的問題, 然 ...
原文:https://www.codeproject.com/articles/85296/important-uses-of-delegates-and-events 原文作者: Shivprasad koirala
介紹
在這篇文章中, 我們會嘗試著去理解delegate能解決什麼樣的問題, 然後會在實例中去使用。 之後, 我們要進一步理解多播委托的概念以及事件是如何封裝委托的。 最終, 我們要明白事件和委托的不同, 學會如何非同步調用委托。 在文章的最後,我們能能總結出委托的六種重要用處。方法和函數的抽象問題
在講委托之前,讓我們先搞明白委托到底能解決什麼問題。下麵是一個很簡單的類“ClsMaths”, 它只有一個方法“Add”。這個類會被一個簡單的客戶端消費(調用)。假設過了一段時間之後,現在客戶端對ClsMaths這個類有了新的需求: 添加一個"Subtration"方法。那麼,按之前的做法, 我們需要修改客戶端已添加對新方法的調用代碼。 換句話說, ClsMaths的一個新增方法導致了客戶端的重新編譯。 簡單來說, 問題出現了: 功能類和消費類之間存在了緊耦合。所以如何解決? 我們可以選擇使用委托作為中間件(墊片), 消費類不再是直接調用實現類的方法,而是調用一個虛擬指針(委托),讓委托去調用真正的執行方法。這樣,我們就把消費類和具體實現方法解耦了。 譯者註, 他這裡的ClsMaths類只有四個方法 加減乘除, 作者使用了一個委托變數來調用4個方法, 所以這裡確實做到瞭解耦。 稍後你就可以看到因為抽象指針的作用,ClsMath的修改將不會對消費類產生任何影響。 這裡的抽象指針就是委托啦。 /** 題外話,上圖提到的Balsamiq Mockups是一個很棒的軟體, 可以用來畫UI效果圖, 我喜歡用來畫流程圖(稍顯不如visio方便, 但是閱讀和美觀效果完爆之) **/如何創建一個委托
創建一個委托只要四步: 定義, 創建, 引用, 調用(和C# in depth 中的說法一致) 第一步是定義一個和函數有同樣返回類型、輸入參數的委托, 例如下麵的Add函數有2個int類型輸入參數以及一個int類型的輸入參數。1 private int Add(int i,int y) 2 { 3 return i + y; 4 }
對此, 我們可以定義如下的委托:
1 // Declare delegate 2 public delegate int PointetoAddFunction(int i,int y);
註意, 返回類型和輸入類型要相容, 否則會報錯。 下一步就是創建一個委托類型的變數嘍:
1 // Create delegate reference 2 PointetoAddFunction myptr = null;
最後就是調用了:
1 // Invoke the delegate 2 myptr.Invoke(20, 10)
下圖為實例代碼:
如何使用委托解決抽象指針問題
為瞭解耦演算法的變化, 我們使用一個抽象的指針指向所有的演算法:(因為這四個方法的格式是一致的) 第一步, 在實現類中定義一個委托如下:(註意輸入輸出參數的格式)1 public class clsMaths 2 { 3 public delegate int PointerMaths(int i, int y); 4 }
第二步, 定義一個返回委托的函數用以暴露具體實現方法給消費類:
1 public class clsMaths 2 { 3 public delegate int PointerMaths(int i, int y); 4 5 public PointerMaths getPointer(int intoperation) 6 { 7 PointerMaths objpointer = null; 8 if (intoperation == 1) 9 { 10 objpointer = Add; 11 } 12 else if (intoperation == 2) 13 { 14 objpointer = Sub; 15 } 16 else if (intoperation == 3) 17 { 18 objpointer = Multi; 19 } 20 else if (intoperation == 4) 21 { 22 objpointer = Div; 23 } 24 return objpointer; 25 } 26 }
下麵就是完整的代碼, 所有的具體實現函數都被標記為private, 只有委托和暴露委托的函數是public的。
1 public class clsMaths 2 { 3 public delegate int PointerMaths(int i, int y); 4 5 public PointerMaths getPointer(int intoperation) 6 { 7 PointerMaths objpointer = null; 8 if (intoperation == 1) 9 { 10 objpointer = Add; 11 } 12 else if (intoperation == 2) 13 { 14 objpointer = Sub; 15 } 16 else if (intoperation == 3) 17 { 18 objpointer = Multi; 19 } 20 else if (intoperation == 4) 21 { 22 objpointer = Div; 23 } 24 return objpointer; 25 } 26 27 private int Add(int i, int y) 28 { 29 return i + y; 30 } 31 private int Sub(int i, int y) 32 { 33 return i - y; 34 } 35 private int Multi(int i, int y) 36 { 37 return i * y; 38 } 39 private int Div(int i, int y) 40 { 41 return i / y; 42 } 43 }
所以消費類的調用就和具體實現方法沒有耦合了:
1 int intResult = objMath.getPointer(intOPeration).Invoke(intNumber1,intNumber2);
多播委托
在我們之前的例子中,我們已經知道瞭如何創建委托變數和綁定具體實現方法到變數上。但實際上, 我們可以給一個委托附上若幹個具體實現方法。如果我們調用這樣的委托, 那麼附到委托上的函數會順序執行。(至於如果函數有返回值, 那麼只有最後一個函數的返回值會被捕捉到)1 // Associate method1 2 delegateptr += Method1; 3 // Associate Method2 4 delegateptr += Method2; 5 // Invoke the Method1 and Method2 sequentially 6 delegateptr.Invoke();
所以, 我們可以在“發佈者/消費者”模式中使用多播委托。例如, 我們的應用中需要不同類型的錯誤日誌處理方式,當錯誤發生時,我們需要把錯誤信息廣播給不同的組件進行不同的處理。 (如下圖)
多播委托的簡單例子
我們可以通過下麵這個例子更好的理解多播委托。 在這個窗體項目中,我們有“Form1”, “Form2”, “Form3”。 “Form1中有一個多播委托來把動作的影響傳遞到“Form2”和“Form3”中。 在"Form1"中, 我們首先定義一個委托以及委托變數, 這個委托是用來傳遞動作的影響到其他Form中的。1 // Create a simple delegate 2 public delegate void CallEveryOne(); 3 4 // Create a reference to the delegate 5 public CallEveryOne ptrcall=null; 6 // Create objects of both forms 7 8 public Form2 obj= new Form2(); 9 public Form3 obj1= new Form3();
在“Form1”的Form_Load函數中, 我們調用其他的Forms;把其他表單中的CallMe方法附加到“Form1”的委托中。
1 private void Form1_Load(object sender, EventArgs e) 2 { 3 // Show both the forms 4 obj.Show(); 5 obj1.Show(); 6 // Attach the form methods where you will make call back 7 ptrcall += obj.CallMe; 8 ptrcall += obj1.CallMe; 9 }
最終, 我們在"Form1"的按鈕點擊函數中調用委托(多播的):
1 private void button1_Click(object sender, EventArgs e) 2 { 3 // Invoke the delegate 4 ptrcall.Invoke(); 5 }
多播委托的問題 -- 暴露過多的信息
上面例子的第一個問題就是, 消費者並沒有權利來選擇訂閱或是不訂閱,因為這個過程是由“Form1”也就是發佈者來決定的。 我們可以用其他方式, 把委托傳遞給消費者, 讓消費者來決定他們要不要訂閱來自發佈者(Form1)的多播委托。 但是, 這種做法會引發另個問題: 破壞封裝。 如果我們把委托暴露給消費者, 就意味著委托完全裸露在了消費者面前。事件 -- 委托的封裝
事件能解決委托的封裝問題。 事件包裹在委托之外, 使得消費者只能接收但不會有委托的完全控制權。 下圖是對這一概念的圖解: 1. 具體的實現方法被委托抽象和封裝了 2. 委托被多播委托進一步封裝了以提供廣播的效果 3. 事件進一步封裝了多播委托實現事件
我們來把多播委托的例子改造成事件的方式。 第一步是在發佈者“Form1”中定義委托和委托類型的事件; 下麵就是對應的代碼塊,請註意關鍵字event。 我們定義了一個委托“CallEveryone”, 然後定義了一個委托類型的事件“EventCallEveryone”。1 public delegate void CallEveryone(); 2 public event CallEveryone EventCallEveryOne;
從發佈者“Form1”中創建“Form2”和“Form3”的對象, 然後把當前這個“Form1”對象傳到“Form2”、 "Form3"中, 這樣 2、 3就可以監聽事件了。
1 Form2 obj = F new Form2(); 2 obj.obj = this; 3 Form3 obj1 = new Form3(); 4 obj1.obj = this; 5 obj.Show(); 6 obj1.Show(); 7 EventCallEveryOne();
在消費者這邊, “Form2”和“Form3”自主決定是否把具體某個方法付到事件上。
1 obj.EventCallEveryOne += Callme;
這段代碼的執行結果將會和我們上文的多播委托的例子結果一樣。
委托和事件的不同
所以, 如果事件不是委托的語法糖那麼他們之間的區別在哪? 我們在上文中已經提到了一個主要的區別: 事件比委托多了一層封裝。因此, 如果我們傳遞委托, 那麼消費者接受的是一個赤裸裸的委托, 用戶可以修改委托的信息。 而我們使用事件,那麼用戶只能監聽事件而不能修改它。 函數的非同步委托調用 委托的另一種用法是非同步函數的調用。 你能夠非同步的調用委托指向的函數。 非同步調用意味著客戶端調用委托之後, 代碼的控制權又立即回到了客戶端手中以繼續執行後續的代碼。 委托攜帶者調用者的信息在parallel的線程池中啟用新的線程執行具體的函數, 當委托執行結束後, 它會發出信息通知客戶端(調用者)。 為了能夠非同步得調用函數, 我們需要call “begininvoke”方法。 在“begininvoke”方法中, 我們需要為委托提供一個回調函數。 如下圖的CallbackMethod。1 delegateptr.BeginInvoke(new AsyncCallback(CallbackMethod), delegateptr);
下麵的代碼段就是一個回調函數的demo, 這段代碼會在委托中的函數執行完成之後被立即調用。
1 static void CallbackMethod(IAsyncResult result) 2 { 3 int returnValue = flusher.EndInvoke(result); 4 }
總結委托的用法
委托有5種重要的使用方式:(譯者註: 原文寫的6種, 我只看到了5種) 1. 抽象、封裝一個方法(匿名調用) 這是委托最重要的功能, 它幫助我們定義一個能夠指向函數的抽象的指針。 同時, 這個指針還可以指向其他符合其規範的函數。開頭的時候我們展示的一個math類, 之後我們使用一個抽象指針就把該math類中添加的所有函數都包括在內了。 這個例子就很好的說明瞭委托的這一使用方法。 2. 回調機制 客戶端可以使用委托來穿件回調函數。
3. 非同步執行 使用BeginInvoke 和 EndInvoke 我們可以非同步得調用所有的委托。
4. 多點廣播 有的時候, 我們希望能夠讓函數順序執行, 那麼多播委托就可以做到這一點。
5. 事件 事件可以幫助我們方便的建立 發佈/訂閱 模式。