2019年7月9日 星期二

Head First C# [Chap11] Events and Delegate

一、何謂 Delegate
   各位都清楚 C/C++ 的函式指標(function pointer),函式指標提供了開發者相當大的彈性,尤其在於功能選單實作,開發者可藉由改變指標指向的函式,輕鬆又漂亮地做出效果,不過函式指標也是 C++ 程式開發中很容易出錯的一環,一個最簡單的情況:函式指標也許在執行過程中指向了不正確的位置、或是在尚未指定任何函式前就先行呼叫函式指標。Delegate 則具備 type-safe、secure managed 的特性,以確保 delegate 指向存在的函式(方法),若在 delegate 尚未存放任何方法即進行呼叫,則 CLR 會拋出一個例外狀況。
  更精準來說:Delegate 是 C# 的特殊型別,用來定義方法(method/function)的 signature,delegate 的實體(instance)可以存放一或多個符合該 signature 的方法。這樣講也許還是鴨子聽雷,不如我們實際來感受一下吧!
Step 1:定義 delegate 型別
在任何類別(class)外定義一個 delegate 型別(注意關鍵字 delegate)
public delegate string GetSecretIngredient(int amount);
這個 delegate 可被用來建立變數,指向任何「接受一個 int 參數並且回傳 string」的方法。
Step 2:建立 delegate 實體
建立類別包含接受一個 int 參數並且回傳 string 的方法
class Suzanne {
 public static string SecretMethod(int amount) {
  return "Suzanne's method " + amount;
 }
 public static void Main() {
     // 建立 delegate 實體並指向 SecretMethod
  GetSecretIngredient MySecretMethod = new GetSecretIngredient(SecretMethod);
  // 可改成:  GetSecretIngredient MySecretMethod = SecretMethod;
  Console.WriteLine(MySecretMethod(5));
 }
}
執行之後,我們可以看到印出:
Suzanne's method 5
  與 C/C++ 的函式指標用法是不是很雷同呢?但是 delegate 的能力並不是只有這樣,它可以掛載更多的方法,只要透過「+=」運算就可以做到。將上述的範例改成如下:
public delegate string GetSecretIngredient(int amount);
class Suzanne {
 public static string SecretMethod1(int amount) {
  Console.WriteLine("Suzanne's method1 " + (amount + 1));
  return "Down";
 }
 public static string SecretMethod2(int amount) {
  Console.WriteLine("Suzanne's method1 " + (amount + 2));
  return "Down";
 }
 public static void Main() {
     // 建立 delegate 實體並指向 SecretMethod
  GetSecretIngredient MySecretMethod = new GetSecretIngredient(SecretMethod1);
  // 可改成:  GetSecretIngredient MySecretMethod = SecretMethod1;
  MySecretMethod += SecretMethod2;
  Console.WriteLine(MySecretMethod(5));
 }
}
很神奇地,執行依序印出了:
Suzanne's method1 6
Suzanne's method2 7
Down
這裡有一個重點,就是「依序」,一旦 delegate 被呼叫,它會依照指定的順序執行方法
  • 我知道「A += B」是「A = A + B」的快速寫法,delegate 也可以用後者的寫法嗎?
  • 答案是肯定的。在上述的例子中,若我們把 MySecretMethod += SecretMethod2 改成 MySecretMethod = SecretMethod2 + MySecretMethod ,那麼則會先執行 SecretMethod2 再執行 SecretMethod1。
  • 如果我指定了不符合 delegate 條件(參數、回傳值)的方法給它,那麼會發生什麼事?
  • 當然是 compile error!如果在 delegate 尚未指定方法即呼叫它,一樣會是 compile error,事實上 C# 中即使是一個 int 變數,在尚未指定任何值就使用它,你也會得到編譯錯誤!你可以自己試試。
二、事件處理
  看到這裡,相信大家都知道什麼是 delegate 了吧!但是什麼情況我們會需要用到它?最直觀的例子就是根據選擇的項目執行不同的方法,例如有 A、B、C 三個方法,如果使用者選擇了 項目1 則執行 A,項目2 則執行 B,項目3 則執行 A 和 C,那麼我們可以利用 delegate 針對每個不同的選項指定不同的方法完成需求。除此之外,C# 的事件處理機制也是藉由 delegate 完成的。
  那到底什麼是「事件(Event)」,事件就是某一件發生在程式裡的事,其他物件可以針對該事件做出回應。這裡我套用一個 Head First C# 裡面所舉的例子:
  你正在撰寫棒球模擬程式,建立了 Ball(球)、Pitcher(投手)、Umpire(裁判)、Fan(球迷)四個類別。當 Ball 被擊出時,它引發了一個 BallInPlay 事件,這時 Umpire 要判斷球是否出界或被接殺,Pitcher 與 Fan(若是全壘打)都會企圖去接那個球,因此,Pitcher、Umpire、Fan 都必須對 BallInPlay 事件做出回應,都必須訂閱(subscribe)這個事件...。
  沒錯!也許有人已經想到 delegate 是如何幫助 C# 的事件處理,Pitcher、Umpire、Fan 都各自擁有不同的事件處理方法,而 Ball 擁有可指向這三個(或者更多)事件處理方法的 delegate,「訂閱」的動作就是使用「+=」運算子,產生 BallInPlay 事件就是呼叫 delegate。我們看看下面這段程式碼吧!
class BallEventArgs : EventArgs {        // BallEventArgs class 繼承 EventArgs
 public int Trajectory { get; private set; }
 public int Distance { get; private set; }
 public BallEventArgs (int Trajectory, int Distance) {
  this.Trajectory = Trajectory;
  this.Distance = Distance;
 }
}
class Ball {  // Ball class
 public event EventHandler BallInPlay;
 // 事件通常為公用的,因此標示為 public
 // event 為關鍵字,一般 delegate 可為區域變數或成員變數,delegate 呼叫前若未指定任何方法,則會造成編譯錯誤,且第一次指定方法不可使用「+=」
 // 標示為 event 者僅能是成員變數,呼叫前若未指定任何方法,則僅在執行期間拋出例外,並且第一次指定方法可使用「+=」
 // EventHandler 則為 delegate 定義:  public delegate void EventHandler(object sender, EventArgs e);
 public void OnBallInPlay (BallEventArgs e) {
  if (BallInPlay != null)
   BallInPlay (this, e);    // 觸發 BallInPlay 事件
 }
}
class Pitcher {   // Pitcher class
 public Pitcher (Ball ball) {
  ball.BallInPlay += new EventHandler(ball_BallInPlay);   // 訂閱 BallInPlay 事件
 }
 void ball_BallInPlay (object sender, EventArgs e) {   // BallInPlay 事件處理方法
  if (e is BallEventArgs) {        // 判斷物件是否為 BallEventArgs 實體
   BallEventArgs ballEventArgs = e as BallEventArgs;  // 將物件由 EventArgs 轉型 BallEventArgs
   if ((ballEventArgs.Distance < 95) && (ballEventArgs.Trajectory < 60))
    CatchBall();
   else
    CoverFirstBase();
  }
 }
}
  在上述的程式中,一旦 OnBallInPlay 函數被呼叫就代表 BallInPlay 事件發生,並且可透過 BallInPlay (this, e) 呼叫所有訂閱該事件的事件處理器。
  事實上,我們常用的 buttonClick 事件也是使用這樣的流程,只是類似 OnBallInPlay 這樣的方法是由 .NET 所呼叫。
三、回呼(Callback)
  只有這樣嗎?Delegate 還可以達到 Callback 的效果。這不難理解,很類似 event 的作法,但是 Callback method 往往只有一個,然而 Event handler 卻有很多個,如何做到確認 delegate 所指向的方法數量,最重要的要訣就是將 delegate 標示為 private,並且以「=」代替「+=」。這樣還不夠,將修飾子標為 private 還是可以被相同類別所產生的實體存取,你必須確保其他實體不會破壞搞壞你的 delegate。你可能覺得這沒有什麼問題,只要我把封裝性做好、類別寫好,我的 delegate 就可以被保護的好好的,但是別忘了還有「擴展方法(Extension Methods)」,別人可能擴展你的類別搞壞你的封裝性,因此標示為 private 只能防君子,不能防小人,目前看來,限制 delegate 指向的方法數量是不可行的。
  本書對 event 與 callback 所做的定義:
事件與回呼之間的重大差別是 - 事件是類別向全世界發布某特定事項已經發生的方式;另一方面,回呼不會被發布,它是私有的,進行此呼叫的方法對於它要呼叫誰有絕對的控制權。
這裡就是我閱讀此書最感到疑惑之處。

from : https://blog.xuite.net/autosun/study/32614006-%5BC%23%5D+%E4%BA%8B%E4%BB%B6%E8%88%87%E5%A7%94%E6%B4%BE%EF%BC%88Delegate%EF%BC%89

沒有留言:

張貼留言