.net程式寫久了,常會看到委派(delegate),但這個名稱實在有點玄,MSDN的解說也讓人百思不解,中文字都看得懂,但兜在一起就變成天書了...網路上搜尋到的解說也都太複雜,範例也用不切實際的例子來當解說,讓人更不了解。直到最近自己寫的一個專案,不得不用到委派,所以自己詳細的研究了一下,總算是了解了一些端倪,就筆記一下,希望能對委派苦手有些幫助。(因為是我自己的了解,所以有不完善或有錯誤請包涵)
委派最常見的用處,就是將我們自己的function當成參數,傳到另一個function來跑。
通常我們的function,傳的參數不外乎是物件,像string、int,或我們自己撰寫的類別實體。但某些時候,在function內需要動態的透過其他function來完成完全不一樣的事情,我們可以有兩種寫法:在function內用一堆if判斷,透過識別參數在不同的if區塊完成各自的動作;另一則是利用委派,所有功能放在同一function內,而獨立處理的功能則當成參數傳遞進去。
一的好處是程式好寫,但缺點就是當各自要做的事很複雜時,程式碼會變得太長;而要增加新功能時,又要再多很多if,久了就很難維護。
而利用委派的方式則解決上面的問題,因為不同功能有各自存在的地方,共同部分完全不用變,所以要維護就變得很簡單。
好了,簡單概念講完,不免俗的馬上來個範例。這裡不討論C#不同版本對於委派不同做法的差異,免得一堆版本一堆程式碼看得頭昏眼花,只會說明什麼時候可以用委派,概念知道了再去深入了解不同版本的做法比較好。
想像情境,有人來跟你借錢,我們一定會看對象再決定要不要借,而決定要借的話,又會依對象會有不同反應及動作,及最後決定借多少錢。這個情境就來當成我們的示範範例:
- 正妹跟你借30萬
- 死黨跟你借100
- 魯蛇跟你借10塊錢
我們先來寫個借錢的動作,裡面就是決定借或不借,要借多少錢,以及借之前的動作:
/// <summary> /// 借錢動作 /// </summary> private void LendAction(string amount) { txtResult.Text = string.Empty; //決定要借出的金額 string finalAmount; string commonRes; if (!string.IsNullOrEmpty(finalAmount)) { //如果金額不是空白就要借出錢 commonRes = string.Format("借出{0}元", finalAmount); } else { commonRes = "掉頭就走"; } txtResult.Text += commonRes; }
這樣每個人借錢的事件就可以通用這個借錢動作:
/// <summary> /// 正妹來借錢了 /// </summary> private void btnGirl_Click(object sender, EventArgs e) { LendAction("30萬"); } /// <summary> /// 死黨來借錢了 /// </summary> private void btnFriend_Click(object sender, EventArgs e) { LendAction("100"); } /// <summary> /// 魯蛇來借錢了 /// </summary> private void btnLoser_Click(object sender, EventArgs e) { LendAction("10"); }
看到這裡,會有個疑問,那怎麼決定要借多少錢,以及借錢之前的動作呢?(也就是finalAmount的值從哪來)
我們可以在這個借錢動作裡寫一堆if來判斷並執行自訂動作,但萬一動作非常的多,那整個function會變得非常的長,日後如果有再多新對象,會非常不好維護。
換個思維,如果我們針對不同對象,有各自獨立的執行自訂動作與判斷金額的function,交由他們來判斷並執行自訂動作就好了呀!
決定之後,我們就來撰寫針對不同對象的動作,輸入參數是金額,回傳參數也是金額(為了好示範所以都用string):
/// <summary> /// 借錢給正妹的自訂動作 /// </summary> /// <param name="amount">跟你借的金額</param> /// <returns>決定借出的金額</returns> private string LendToGirl(string amount) { //自訂動作:跟正妹狂聊,最後決定借五百萬 var res = @"詢問正妹:真的只要借{0}嗎?夠不夠啊? 詢問正妹:要幫妳買點數卡嗎? 詢問正妹:可以加妳的Line嗎? 詢問正妹:妳幾歲呀? 詢問正妹:妳住哪? 詢問正妹:妳有男朋友嗎? 詢問正妹:妳三圍多少? 詢問正妹:禮拜六有空嗎? ... ..... .... 哇!服務這麼好喔! .... ..... GGInInDer OK~{1}沒問題! .... ...去提款機領{1}元 "; var finalAmount = "五百萬"; txtResult.Text = string.Format(res, amount, finalAmount); //回傳最後決定的金額 return finalAmount; } /// <summary> /// 借錢給死檔的自訂動作 /// </summary> /// <param name="amount"></param> /// <returns></returns> private string LendToFriend(string amount) { //自訂動作:馬上就決定 var res = @"幹... (錢包掏出{0}元) "; txtResult.Text = string.Format(res, amount); return amount; } /// <summary> /// 借錢給魯蛇的自訂動作 /// </summary> /// <param name="amount"></param> /// <returns></returns> private string LendToLoser(string amount) { //自訂動作:什麼都不做 return string.Empty; }
寫好之後我們就可以把這些自訂動作(function)當作參數傳給主要借錢動作,讓他去判斷並執行,這樣我們就不用傷腦筋了。
要怎麼做呢?委派就來囉!
首先要定義好委派,不用想的太複雜,就當成定義介面一樣,委派也是規定這個實體的回傳型態、要傳入哪些參數,只是把interface關鍵字換成delegate:
delegate string CustomAction(string amount);
好了之後就可以產生他的參考:
CustomAction customAction;
接著修改原有的借錢動作,加入一個東西讓她幫助我們執行不同的判斷:
/// <summary> /// 借錢動作 /// </summary> /// <param name="amount"></param> /// <param name="customAct"></param> private void LendAction(string amount, CustomAction customAct) { txtResult.Text = string.Empty; //決定要借出的金額 string finalAmount; //我們不需要知道這個customAct到底是什麼 //反正他跑完會回傳一個我們要的東西就對了 //在這裡回傳的就是最終借出金額 finalAmount = customAct(amount); string commonRes; if (!string.IsNullOrEmpty(finalAmount)) { commonRes = string.Format("借出{0}元", finalAmount); } else { commonRes = "掉頭就走"; } txtResult.Text += commonRes; }
最後,我們要決定該傳入哪個判斷進去。回到不同人不同的處理方法中去指定:
/// <summary> /// 正妹來借錢了 /// </summary> private void btnGirl_Click(object sender, EventArgs e) { //只要是符合委派格式的function,就可以指定給他 customAction = LendToGirl; LendAction("30萬", customAction); } /// <summary> /// 死黨來借錢了 /// </summary> private void btnFriend_Click(object sender, EventArgs e) { customAction = LendToFriend; LendAction("100", customAction); } /// <summary> /// 魯蛇來借錢了 /// </summary> private void btnLoser_Click(object sender, EventArgs e) { customAction = LendToLoser; LendAction("10", customAction); }
可以這樣就完成我們想要的動作了。看看結果:
魯蛇借錢囉:
死黨借錢囉:
正妹借錢囉:
使用委派處理這類型的問題好處多多,哪怕改天又多出了老闆、老師、老婆、老公、老爸老媽老哥老姊老弟老妹跟你借錢,主功能LendAction()程式碼包含參數完全不用動,只要在各個事件中將處理function及委派參考設定好就可以了,簡單好維護!!
完整的程式碼:
public partial class Form1 : Form { /// <summary> /// 定義一個自訂借錢動作的委派(想像成是定義一個介面,規定參數及回傳型態就對了) /// </summary> /// <param name="amount"></param> /// <returns></returns> delegate string CustomAction(string amount); /// <summary> /// 產生委派的參考 /// </summary> CustomAction customAction; public Form1() { InitializeComponent(); } /// <summary> /// 正妹來借錢了 /// </summary> private void btnGirl_Click(object sender, EventArgs e) { //只要是符合委派格式的function,就可以指定給他 customAction = LendToGirl; LendAction("30萬", customAction); } /// <summary> /// 死黨來借錢了 /// </summary> private void btnFriend_Click(object sender, EventArgs e) { customAction = LendToFriend; LendAction("100", customAction); } /// <summary> /// 魯蛇來借錢了 /// </summary> private void btnLoser_Click(object sender, EventArgs e) { customAction = LendToLoser; LendAction("10", customAction); } /// <summary> /// 借錢動作 /// </summary> /// <param name="amount"></param> /// <param name="customAct"></param> private void LendAction(string amount, CustomAction customAct) { txtResult.Text = string.Empty; //決定要借出的金額 string finalAmount; //我們不需要知道這個customAct到底是什麼 //反正他跑完會回傳一個我們要的東西就對了 //在這裡回傳的就是最終借出金額 finalAmount = customAct(amount); string commonRes; if (!string.IsNullOrEmpty(finalAmount)) { commonRes = string.Format("借出{0}元", finalAmount); } else { commonRes = "掉頭就走"; } txtResult.Text += commonRes; } /// <summary> /// 借錢給正妹的自訂動作 /// </summary> /// <param name="amount"></param> /// <returns></returns> private string LendToGirl(string amount) { //自訂動作:跟正妹狂聊,最後決定借五百萬 var res = @"詢問正妹:真的只要借{0}嗎?夠不夠啊? 詢問正妹:要幫妳買點數卡嗎? 詢問正妹:可以加妳的Line嗎? 詢問正妹:妳幾歲呀? 詢問正妹:妳住哪? 詢問正妹:妳有男朋友嗎? 詢問正妹:妳三圍多少? 詢問正妹:禮拜六有空嗎? ... ..... .... 哇!服務這麼好喔! .... ..... GGInInDer OK~{1}沒問題! .... ...去提款機領{1}元 "; var finalAmount = "五百萬"; txtResult.Text = string.Format(res, amount, finalAmount); //回傳最後決定的金額 return finalAmount; } /// <summary> /// 借錢給死檔的自訂動作 /// </summary> /// <param name="amount"></param> /// <returns></returns> private string LendToFriend(string amount) { //自訂動作:馬上就決定 var res = @"幹... (錢包掏出{0}元) "; txtResult.Text = string.Format(res, amount); return amount; } /// <summary> /// 借錢給魯蛇的自訂動作 /// </summary> /// <param name="amount"></param> /// <returns></returns> private string LendToLoser(string amount) { //自訂動作:什麼都不做 return string.Empty; } }
完整VS專案也可以在這裡下載。
當然這只是委派其中之一的使用時機,或許我的例子還是舉的不太好,但實際動手做過就會知道大概的原理,了解之後程式的寫法就會有更大的彈性!!
--
補充:
同事看到這個例子來跟我討論,由於事件數量過小,而且所有function全在同一個class,有可能看不出優點在哪。
但想像一下,假設今天來借錢的人有1000個,那主function的if數量會多到驚人!而改用委派的話,我們可以將事件和自訂處理放在同一個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# 參考)
非常有趣生動的比喻,一目瞭然,感謝
回覆刪除感謝您😄
刪除你前面寫一大串借錢的舉例,還不如直接講解你專案的東西,專案裡需要實現的功能,才是最實在的例子。
回覆刪除同學你知道保密條約嗎 太深度的又不好理解
刪除兩年前看不懂,今天偶然點到突然懂了!XD
回覆刪除真是太好了XD
刪除寫了12年的C# (從VS2005 開始用) ,從來沒有用過委派功能,因為,根本不曉得這是什麼。
回覆刪除今天看了您的大作,才明白原來是這回事,感謝您的辛勞,謝謝。
別這麼說~~大家一開始也都不熟
刪除尤其委派對於沒接觸過的人真的是挺玄的一個功能
但理解了之後其實就沒那麼困難了~
尤其現在的Lambda運算式用到的地方非常廣泛,他們也都是由委派構成
像Func或是Action,都是委派更簡化的用法,理解了好處多多,程式也可以更簡單漂亮
最近剛入行軟體業,公司的 Project 大量使用到委派及事件的觀念,看完您的解說後恍然大悟,由衷感謝您!
回覆刪除恭喜~其實委派在現在C#程式裡很常用到,所以理解基本觀念是很重要的:)
刪除最後一個舉例不是應該是【 class 死黨 】嗎?怎麼變成千人斬 XD (重點誤w
回覆刪除謝謝文章的分享~ 概念清楚很多~
很高興此文章能有幫助~哈哈XD
刪除淺顯易懂易了解 謝謝
回覆刪除感謝您~
刪除發現 還不少人跟我一樣,讓我苦腦很久的問題終於有點懂了,最近因為有用到Lambda 進而回追到委派,現在終於可以在從委派去理解Lambda了,感謝大大
回覆刪除很高興能幫助到您!大家一起努力😄
刪除很棒的文章
回覆刪除比較懂要怎麼用了
我看了之後,還在理解中,並想辦法看看能不能實用進去。非常感謝你的講解。
回覆刪除很棒! 感謝分享
回覆刪除感謝分享
回覆刪除請問
private void LendAction(string amount, CustomAction customAct)// 借錢動作
這個函數 其中的參數有 CustomAction customAct 是不是也是一種委派的寫法
或者 應該如何理解呢
初學 見諒
是的,這就是把自訂的function當作參數傳進去,如此這個函數不需要知道他到底會做哪些事,只要呼叫他,並接收指定型態的回傳就好了,也就是把工作委派給另外的函數來做,所以才叫做委派
刪除感謝分享 最近在學,我也找了大致相同的委派 但是f5過不了
回覆刪除class Program
{
public delegate void TestDelegate();
TestDelegate testDelegateFunction; // 產生委派的參考
static void Main(string[] args)
{
testDelegateFunction = MyTestDelegateFunction;
testDelegateFunction();
Console.ReadKey();
}
private static void MyTestDelegateFunction()
{
Console.WriteLine(" MyTestDelegateFunction");
}
}
謝謝
因為你定義的 TestDelegate testDelegateFunction; 這一行是非static,無法在static function裡面用
刪除改為static TestDelegate testDelegateFunction; 就可以囉!
很感謝回答, 我試了就ok了,只是如果我把 private void MyTestDelegateFunction() 的static 拿掉且TestDelegate testDelegateFunction;也不加static,f%之後又是同樣的錯誤,但是查看你的code內完全沒有static就可以跑,原因又是為何呢?
回覆刪除初學者很多觀念不懂 見諒
笑了,真的好懂,感謝分享。 不過要用在自己的案子還要思考轉化一下 orz
回覆刪除感謝您不嫌棄 XD
刪除