2015-01-14

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


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

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



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

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

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

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

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

  1. 正妹跟你借30萬
  2. 死黨跟你借100
  3. 魯蛇跟你借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 魯蛇

  • 魯蛇借錢事件
  • 魯蛇借錢自訂動作
...
.....
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# 參考)