2017年2月6日 星期一

.Net委派(delegate)的簡易解說與用法

.net程式寫久了,常會看到委派(delegate),但這個名稱實在有點玄,MSDN的解說也讓人百思不解,中文字都看得懂,但兜在一起就變成天書了...網路上搜尋到的解說也都太複雜,範例也用不切實際的例子來當解說,讓人更不了解。直到最近自己寫的一個專案,不得不用到委派,所以自己詳細的研究了一下,總算是了解了一些端倪,就筆記一下,希望能對委派苦手有些幫助。(因為是我自己的了解,所以有不完善或有錯誤請包涵)

委派最常見的用處,就是將我們自己的function當成參數,傳到另一個function來跑。



通常我們的function,傳的參數不外乎是物件,像string、int,或我們自己撰寫的類別實體。但某些時候,在function內需要動態的透過其他function來完成完全不一樣的事情,我們可以有兩種寫法:在function內用一堆if判斷,透過識別參數在不同的if區塊完成各自的動作;另一則是利用委派,所有功能放在同一function內,而獨立處理的功能則當成參數傳遞進去。

一的好處是程式好寫,但缺點就是當各自要做的事很複雜時,程式碼會變得太長;而要增加新功能時,又要再多很多if,久了就很難維護。

而利用委派的方式則解決上面的問題,因為不同功能有各自存在的地方,共同部分完全不用變,所以要維護就變得很簡單。

好了,簡單概念講完,不免俗的馬上來個範例。這裡不討論C#不同版本對於委派不同做法的差異,免得一堆版本一堆程式碼看得頭昏眼花,只會說明什麼時候可以用委派,概念知道了再去深入了解不同版本的做法比較好。

想像情境,有人來跟你借錢,我們一定會看對象再決定要不要借,而決定要借的話,又會依對象會有不同反應及動作,及最後決定借多少錢。這個情境就來當成我們的示範範例:

  1. 正妹跟你借30萬
  2. 死黨跟你借100
  3. 魯蛇跟你借10塊錢



我們先來寫個借錢的動作,裡面就是決定借或不借,要借多少錢,以及借之前的動作:
  1. /// <summary>
  2. /// 借錢動作
  3. /// </summary>
  4. private void LendAction(string amount) {
  5. txtResult.Text = string.Empty;
  6.  
  7. //決定要借出的金額
  8. string finalAmount;
  9.  
  10. string commonRes;
  11. if (!string.IsNullOrEmpty(finalAmount)) { //如果金額不是空白就要借出錢
  12. commonRes = string.Format("借出{0}元", finalAmount);
  13. }
  14. else {
  15. commonRes = "掉頭就走";
  16. }
  17.  
  18. txtResult.Text += commonRes;
  19. }

這樣每個人借錢的事件就可以通用這個借錢動作: 
  1. /// <summary>
  2. /// 正妹來借錢了
  3. /// </summary>
  4. private void btnGirl_Click(object sender, EventArgs e) {
  5. LendAction("30萬");
  6. }
  7.  
  8. /// <summary>
  9. /// 死黨來借錢了
  10. /// </summary>
  11. private void btnFriend_Click(object sender, EventArgs e) {
  12. LendAction("100");
  13. }
  14.  
  15. /// <summary>
  16. /// 魯蛇來借錢了
  17. /// </summary>
  18. private void btnLoser_Click(object sender, EventArgs e) {
  19. LendAction("10");
  20. }

看到這裡,會有個疑問,那怎麼決定要借多少錢,以及借錢之前的動作呢?(也就是finalAmount的值從哪來)

我們可以在這個借錢動作裡寫一堆if來判斷並執行自訂動作,但萬一動作非常的多,那整個function會變得非常的長,日後如果有再多新對象,會非常不好維護。

換個思維,如果我們針對不同對象,有各自獨立的執行自訂動作與判斷金額的function,交由他們來判斷並執行自訂動作就好了呀!

決定之後,我們就來撰寫針對不同對象的動作,輸入參數是金額,回傳參數也是金額(為了好示範所以都用string):
  1. /// <summary>
  2. /// 借錢給正妹的自訂動作
  3. /// </summary>
  4. /// <param name="amount">跟你借的金額</param>
  5. /// <returns>決定借出的金額</returns>
  6. private string LendToGirl(string amount) {
  7. //自訂動作:跟正妹狂聊,最後決定借五百萬
  8. var res =
  9. @"詢問正妹:真的只要借{0}嗎?夠不夠啊?
  10. 詢問正妹:要幫妳買點數卡嗎?
  11. 詢問正妹:可以加妳的Line嗎?
  12. 詢問正妹:妳幾歲呀?
  13. 詢問正妹:妳住哪?
  14. 詢問正妹:妳有男朋友嗎?
  15. 詢問正妹:妳三圍多少?
  16. 詢問正妹:禮拜六有空嗎?
  17. ...
  18. .....
  19. ....
  20. 哇!服務這麼好喔!
  21. ....
  22. .....
  23. GGInInDer
  24. OK~{1}沒問題!
  25. ....
  26. ...去提款機領{1}元
  27. ";
  28. var finalAmount = "五百萬";
  29. txtResult.Text = string.Format(res, amount, finalAmount);
  30.  
  31. //回傳最後決定的金額
  32. return finalAmount;
  33. }
  34.  
  35. /// <summary>
  36. /// 借錢給死檔的自訂動作
  37. /// </summary>
  38. /// <param name="amount"></param>
  39. /// <returns></returns>
  40. private string LendToFriend(string amount) {
  41. //自訂動作:馬上就決定
  42. var res =
  43. @"幹...
  44. (錢包掏出{0}元)
  45. ";
  46. txtResult.Text = string.Format(res, amount);
  47. return amount;
  48. }
  49.  
  50. /// <summary>
  51. /// 借錢給魯蛇的自訂動作
  52. /// </summary>
  53. /// <param name="amount"></param>
  54. /// <returns></returns>
  55. private string LendToLoser(string amount) {
  56. //自訂動作:什麼都不做
  57. return string.Empty;
  58. }


寫好之後我們就可以把這些自訂動作(function)當作參數傳給主要借錢動作,讓他去判斷並執行,這樣我們就不用傷腦筋了。

要怎麼做呢?委派就來囉!

首先要定義好委派,不用想的太複雜,就當成定義介面一樣,委派也是規定這個實體的回傳型態、要傳入哪些參數,只是把interface關鍵字換成delegate: 
  1. delegate string CustomAction(string amount);

好了之後就可以產生他的參考: 
  1. CustomAction customAction;

接著修改原有的借錢動作,加入一個東西讓她幫助我們執行不同的判斷: 
  1. /// <summary>
  2. /// 借錢動作
  3. /// </summary>
  4. /// <param name="amount"></param>
  5. /// <param name="customAct"></param>
  6. private void LendAction(string amount, CustomAction customAct) {
  7. txtResult.Text = string.Empty;
  8.  
  9. //決定要借出的金額
  10. string finalAmount;
  11.  
  12. //我們不需要知道這個customAct到底是什麼
  13. //反正他跑完會回傳一個我們要的東西就對了
  14. //在這裡回傳的就是最終借出金額
  15. finalAmount = customAct(amount);
  16. string commonRes;
  17. if (!string.IsNullOrEmpty(finalAmount)) {
  18. commonRes = string.Format("借出{0}元", finalAmount);
  19. }
  20. else {
  21. commonRes = "掉頭就走";
  22. }
  23.  
  24. txtResult.Text += commonRes;
  25. }


最後,我們要決定該傳入哪個判斷進去。回到不同人不同的處理方法中去指定: 
  1. /// <summary>
  2. /// 正妹來借錢了
  3. /// </summary>
  4. private void btnGirl_Click(object sender, EventArgs e) {
  5. //只要是符合委派格式的function,就可以指定給他
  6. customAction = LendToGirl;
  7. LendAction("30萬", customAction);
  8. }
  9.  
  10. /// <summary>
  11. /// 死黨來借錢了
  12. /// </summary>
  13. private void btnFriend_Click(object sender, EventArgs e) {
  14. customAction = LendToFriend;
  15. LendAction("100", customAction);
  16. }
  17.  
  18. /// <summary>
  19. /// 魯蛇來借錢了
  20. /// </summary>
  21. private void btnLoser_Click(object sender, EventArgs e) {
  22. customAction = LendToLoser;
  23. LendAction("10", customAction);
  24. }


可以這樣就完成我們想要的動作了。看看結果:
魯蛇借錢囉:

死黨借錢囉:

正妹借錢囉:


使用委派處理這類型的問題好處多多,哪怕改天又多出了老闆、老師、老婆、老公、老爸老媽老哥老姊老弟老妹跟你借錢,主功能LendAction()程式碼包含參數完全不用動,只要在各個事件中將處理function及委派參考設定好就可以了,簡單好維護!!

完整的程式碼: 
  1. public partial class Form1 : Form
  2. {
  3. /// <summary>
  4. /// 定義一個自訂借錢動作的委派(想像成是定義一個介面,規定參數及回傳型態就對了)
  5. /// </summary>
  6. /// <param name="amount"></param>
  7. /// <returns></returns>
  8. delegate string CustomAction(string amount);
  9.  
  10. /// <summary>
  11. /// 產生委派的參考
  12. /// </summary>
  13. CustomAction customAction;
  14.  
  15. public Form1() {
  16. InitializeComponent();
  17. }
  18.  
  19. /// <summary>
  20. /// 正妹來借錢了
  21. /// </summary>
  22. private void btnGirl_Click(object sender, EventArgs e) {
  23. //只要是符合委派格式的function,就可以指定給他
  24. customAction = LendToGirl;
  25. LendAction("30萬", customAction);
  26. }
  27.  
  28. /// <summary>
  29. /// 死黨來借錢了
  30. /// </summary>
  31. private void btnFriend_Click(object sender, EventArgs e) {
  32. customAction = LendToFriend;
  33. LendAction("100", customAction);
  34. }
  35.  
  36. /// <summary>
  37. /// 魯蛇來借錢了
  38. /// </summary>
  39. private void btnLoser_Click(object sender, EventArgs e) {
  40. customAction = LendToLoser;
  41. LendAction("10", customAction);
  42. }
  43.  
  44. /// <summary>
  45. /// 借錢動作
  46. /// </summary>
  47. /// <param name="amount"></param>
  48. /// <param name="customAct"></param>
  49. private void LendAction(string amount, CustomAction customAct) {
  50. txtResult.Text = string.Empty;
  51.  
  52. //決定要借出的金額
  53. string finalAmount;
  54.  
  55. //我們不需要知道這個customAct到底是什麼
  56. //反正他跑完會回傳一個我們要的東西就對了
  57. //在這裡回傳的就是最終借出金額
  58. finalAmount = customAct(amount);
  59. string commonRes;
  60. if (!string.IsNullOrEmpty(finalAmount)) {
  61. commonRes = string.Format("借出{0}元", finalAmount);
  62. }
  63. else {
  64. commonRes = "掉頭就走";
  65. }
  66.  
  67. txtResult.Text += commonRes;
  68. }
  69.  
  70. /// <summary>
  71. /// 借錢給正妹的自訂動作
  72. /// </summary>
  73. /// <param name="amount"></param>
  74. /// <returns></returns>
  75. private string LendToGirl(string amount) {
  76. //自訂動作:跟正妹狂聊,最後決定借五百萬
  77. var res =
  78. @"詢問正妹:真的只要借{0}嗎?夠不夠啊?
  79. 詢問正妹:要幫妳買點數卡嗎?
  80. 詢問正妹:可以加妳的Line嗎?
  81. 詢問正妹:妳幾歲呀?
  82. 詢問正妹:妳住哪?
  83. 詢問正妹:妳有男朋友嗎?
  84. 詢問正妹:妳三圍多少?
  85. 詢問正妹:禮拜六有空嗎?
  86. ...
  87. .....
  88. ....
  89. 哇!服務這麼好喔!
  90. ....
  91. .....
  92. GGInInDer
  93. OK~{1}沒問題!
  94. ....
  95. ...去提款機領{1}元
  96. ";
  97. var finalAmount = "五百萬";
  98. txtResult.Text = string.Format(res, amount, finalAmount);
  99.  
  100. //回傳最後決定的金額
  101. return finalAmount;
  102. }
  103.  
  104. /// <summary>
  105. /// 借錢給死檔的自訂動作
  106. /// </summary>
  107. /// <param name="amount"></param>
  108. /// <returns></returns>
  109. private string LendToFriend(string amount) {
  110. //自訂動作:馬上就決定
  111. var res =
  112. @"幹...
  113. (錢包掏出{0}元)
  114. ";
  115. txtResult.Text = string.Format(res, amount);
  116. return amount;
  117. }
  118.  
  119. /// <summary>
  120. /// 借錢給魯蛇的自訂動作
  121. /// </summary>
  122. /// <param name="amount"></param>
  123. /// <returns></returns>
  124. private string LendToLoser(string amount) {
  125. //自訂動作:什麼都不做
  126. return string.Empty;
  127. }
  128. }


完整VS專案也可以在這裡下載。


當然這只是委派其中之一的使用時機,或許我的例子還是舉的不太好,但實際動手做過就會知道大概的原理,了解之後程式的寫法就會有更大的彈性!!

--
補充:

同事看到這個例子來跟我討論,由於事件數量過小,而且所有function全在同一個class,有可能看不出優點在哪。

但想像一下,假設今天來借錢的人有1000個,那主function的if數量會多到驚人!而改用委派的話,我們可以將事件和自訂處理放在同一個class內,這樣的架構就變成如下:

class 正妹

  • 正妹借錢事件
  • 正妹借錢自訂動作
class 魯蛇

  • 魯蛇借錢事件
  • 魯蛇借錢自訂動作
...
.....
class 千人斬

  • 千人斬借錢事件
  • 千人斬借錢自訂動作


而主動作function還是完全不用動,要新增新對象,只要新增class即可;要修改某對象的動作,也只要前往該對象的class內即可輕鬆修改,程式的可讀性更是大大增加。

--

p.s. 會用到委派是因為最近自己寫的讀取EXIF專案,為了處理不同區塊卻有相同名稱的元素的實際值而使用的。

例如區塊A、B、C都有某個叫Z的元素,裡面存放的值不同,但存取方法相同;不過有的值可能需要特殊處理,而區塊未來可能還有D、E、F...更多。

所以我把存取值寫成獨立function(F1),判斷不同區塊需特殊處理的元素即用委派當成參數帶入,如此不同區塊物件在存取Z值的時候全部都可用F1,且F1完全不用動,只要各個物件寫好自己處理特殊元素的function(FF1~FFn),再帶入F1,F1即可處理共通值,或自動呼叫FFx處理特殊值。

在未來簡介EXIF的時候有機會會寫到這個例子。不知道有沒有人會看,但還是敬請期待XD

參考資料: MSDN delegate (C# 參考)

from : https://eric0806.blogspot.tw/2015/01/dotnet-delegate-usage.html

沒有留言:

張貼留言