2011年12月20日 星期二

C#的委派與事件的使用

委派跟事件,一直是蠻難真正搞懂的一個概念,但是對UI寫作的人或是開發控制項的人來說,卻又經常用到,我想寫系統的人或許也有機會碰到吧,過去一段時間,我一直沒辦法完全搞懂,甚至只是很皮毛的瞭解就拿來用了。為了讓自己更有概念一點,這次花了些時間學習,記錄如下。




◆什麼時候會用到委派?

有兩種狀況:
1.你要寫一個Class或Interface,其中有個函式,會讓別的Class呼叫(注意,是Class,不是Instance喔)。也就是在撰寫類別的時候,就能夠呼叫非實體的函式。


2.你要寫一個Class或Interface,其中有個函式,會達成某種特別功能,但是,寫的時候,你並不知道這個功能會怎麼完成,好比說,你這個Class是個登入的介面或Model,但是密碼驗證的部分,是你提供方法出來,給要使用你這個Class的人,到時候要實作函式內容的(將委派綁定函式)。

當你撰寫一個Class裡面需要呼叫該Class無法呼叫到的函式(當然,就不是這個Class宣告的函式囉)的時候,就會需要用到委派,而什麼情況下Class無法呼叫某個函式呢?我們先看看,什麼情況下,Class可以呼叫非Class自己的函式呢?比方有兩個Class,一個是ClassA一個是ClassB,如果在ClassA中宣告產生了ClassB,那麼ClassB的函式就可以被ClassA呼叫(如果宣告成Public或是有封裝出來),這是物件導向的基本概念,相信大家都懂,但是,如果ClassB想呼叫ClassA的函式呢?那就做不到了。程式如下:


Class ClassA
{
     public void RunClassA()
    {
        ClassB MyClassB = new ClassB();
         MyClassB.MethodB();
    }

    public void MethodA()
   {
        Console.WriteLine( "MethodA被呼叫了" );
    }
}



Class ClassB
{
     public void RunClassB()
    {
        ClassA.MethodA();
    }

    public void MethodB()
   {
        Console.WriteLine( "MethodB被呼叫了" );
    }
}




上述紅底的那一行,就是無法做到的,因為ClassB沒有宣告ClassA,對ClassB來說,他不曉得ClassA是什麼,那麼,如果在ClassB也宣告一個ClassA呢?那麼A中有B,B中又有A,A中又有B,無限循環下去記憶體會吃光光,這種怪程式應該無法跑不下去,有解決方法嗎?有啊,除了用Static來做,或是把ClassA傳進來給ClassB......但是,有時候ClassA不是你寫的,你沒辦法預期人家怎麼做,但是卻又有這種需求,這時候就需要委派了。



◆委派怎麼做呢?
先不要講程式,免得把人搞昏了,概念上,就是ClassB這邊,宣告一個叫做「委派」的東西,這個委派,可以讓ClassB被別人宣告使用的時候,也同時可以使用這個委派,透過特殊的語法,在ClassA把委派跟某個函式綁在一起,這樣只要有人(不論ClassB或ClassA都可以)叫這個委派做事,這個委派就會去找到這個函式,然後執行,這樣ClassB就可以呼叫到ClassA了,聽起來似乎落落長,實際上不難,我們來看看怎麼做。


再稍微細節一點來看,這個作法,就很像是把ClassA的函式的記憶體位址告訴ClassB的委派,而委派大概這麼使用的,這個程式語法跟C#原本語法概念不太相同,不好理解,當成公式吧,要用的時候查就好了。


在ClassB裡頭,先宣告委派

        public delegate int DelegateDefine( String MyPara );

好複雜難懂得一行,不急,這邊指示先宣告委派型別,目的是定義好你這個委派會呼叫什麼樣的函式,那個函式有怎樣的參數,又是怎樣的回傳型態。比方說,這裡是宣告說,我有一個委派叫做DelegateDefine,他到時候要呼叫一個「會傳入一個String的函式,然後這函式的回傳值,是int,就這樣而已。


接下來,把這個委派產生成物件


        public DelegateDefine DelegateObj;


接下來ClassA就可以開始把委派跟函式綁在一起,當然,先決條件就是這個委派宣告的函式型態,可得跟ClassA要綁定的函式一樣喔,比方,函式如下


        public int ClassA_Method( String Paras )
        {
            return 1;
        }



然後開始綁定,可以把綁定寫在ClassA的建構子,也可以寫在某個函式裡面,只要能寫在某件事情之前,什麼事情?就是要叫用那個ClassB的委派之前就行了。綁定的語法也是比較特殊,就是把委派物件跟函式綁在一起就是了。



            My_ClassB.DelegateObj = new ClassB.DelegateDefine( ClassA_Method );




看起來有點複雜,有沒有簡化的寫法?有,有,有,經過簡化過後你只需要這樣寫就行了:
            My_ClassB.DelegateObj = ClassA_Method;








之後,不論ClassA或ClassB都可以用DelegateObj.Invoke( "xxx" );來呼叫函式ClassA_Method 了(xxx就是看你到時候要傳什麼當參數囉),就這樣,很簡單吧?






我寫了一個實際的小範例,大概是這樣的,有一個Class是使用者控制項,用來設計UI放元件的,叫做UC_Child,另外,有兩個Class是Form,也是用來設計UI的,分別叫做Form_Mother與Form_Father,UC_Child設計好了之後,會被Form宣告使用,很顯然的,Form就可以呼叫並使用UC_Child寫好的函式。但,這裡我需要讓UC_Child反向來呼叫Form這邊的函式呢?比方UC_Child這邊按下某個按鈕,或是完成收送之後,要通知Form來更新畫面呢?這個更新畫面,當然是只能寫在Form這邊,而無法寫在UC_Child,這時候,就要透過委派了。


這個範例,除了很簡單的做出委派之外,也包含了幾個委派的特性


1.委派可以同時綁兩個以上的函式
2.委派可以讓很多Parent宣告使用
3.委派綁定了函式之後,當呼叫函式時,並不會在遇到Return的時候就走回去,而是會把整個函式跑完的。
4.委派產生之後,不但Child自己可以Invoke,連Parent也可以呼叫Invoke,只不過,Parent用起來就沒什麼太大的實質意義了。
5.委派同時綁兩個以上的函式之後,會每個函式都跑過了,才會回到委派的下一行來。所以在Child裡面的Btn_Delegate_Click()裡,呼叫了DelegateObj.Invoke()之後,即便他綁了兩個函式,也不會回到這邊兩次,只會最後回到這邊來,然後帶著最後一個回傳值喔。




範例放在這邊   跟這邊



那麼,什麼是事件呢?其實大家對事件的接觸跟瞭解應該反而比委派多,就我目前瞭解,委派跟事件幾乎一樣,用法差不多,反而事件還比較複雜一點,據說,事件有多一些保護機制,但是在我瞭解之前,我也不亂多說,在上面的Link當中有Delegate跟Event兩個範例,差異實在很小。


1.原本在Child要把委派產生個實體出來,要用 事件 的方式的話,就不用,變成用以下的特殊語法,產生一個符合那個委派的事件,然後在Father跟Mother改成用事件跟函式綁定


public event DelegateDefine On_Env_Obj;


2.在Father跟Mother原本也可以叫用的Child的委派,現在改用事件的話,無法叫用Child的 事件 喔,也就是程式裡遮掉的那行
  //My_Child.On_Env_Obj.Invoke 不可以





31 則留言:

  1. 精華文章!!! 講解的十分清楚,初學者必看

    回覆刪除
  2. 謝謝你^^,如果我有更多的瞭解,一定會再做補充^^

    回覆刪除
  3. 請問,我已經正確的在form1裡把委派物件跟函式綁在一起了,
    可是在form2要使用DelegateObj.Invoke("xxx");時,
    卻還總是出現DelegateObj是null的狀況呢?

    回覆刪除
  4. 我猜你的「委派與函式綁定」這段沒做好
    [ 我已經正確的在form1裡把委派物件跟函式綁在一起了 ] 我猜你這段做的有問題,把你程式碼貼上來看看?

    回覆刪除
    回覆
    1. 先謝謝你了~~
      我弄了一個很簡單的程式,如下:

      Form1:
      namespace Delegate
      {
      public partial class Form1 : Form
      {
      Form2 tt = new Form2();
      public Form1()
      {
      InitializeComponent();
      tt.DelegateObj = new Form2.DelegateDefine(showmessage);
      }

      private void button1_Click(object sender, EventArgs e)
      {
      Form2 F = new Form2();
      F.Show();
      }

      public void showmessage(string aa)
      {
      label1.Text = aa;
      }
      }
      }

      Form2:
      namespace Delegate
      {
      public partial class Form2 : Form
      {
      public delegate void DelegateDefine(String MyPara);
      public DelegateDefine DelegateObj;

      public Form2()
      {
      InitializeComponent();
      }

      private void button1_Click(object sender, EventArgs e)
      {
      DelegateObj.Invoke("Message from Form2");
      }
      }
      }

      以上,
      我基本上就是照著您的作法。
      可是要執行Form2的button時,
      就會出現錯誤。

      我去trace程式,
      在Form1的tt.DelegateObj = new Form2.DelegateDefine(showmessage);這一行過後,
      可以看到DelegateObj有內容,
      可是執行到Form2的DelegateObj.Invoke("Message from Form2");這一行時,
      卻發現DelegateObj已經變成null了。

      刪除
    2. 我解了!!!
      原來是我自己笨,
      在 Form1 的 button 1 中又 new 了 form2,
      所以這個form2 裡的DelegateObj當然沒有被綁定啦!!
      還是非常謝謝你的網頁說明,
      真的是非常清楚又易懂~~

      刪除
    3. 不好意思,我假日行程滿檔,所以沒能幫你看看
      一早就看到你有好消息
      沒錯,你把Form2 產生在兩個地方,所以按鈕內的就沒有綁定到
      ^_^

      刪除
  5. 以你的form1跟form2來說
    form1裡頭要產生form2物件,然後讓委派綁定事件

    form2 My_form2 = new form2( );


    // 委派與函式綁定
    My_form2 .DelegateObj = new form2.DelegateDefine( form1_Method );

    回覆刪除
  6. 簡化一下用法
    form2 My_form2 = new form2( );

    // 委派與函式綁定
    My_form2 .DelegateObj = form1_Method;

    回覆刪除
  7. 謝謝您的分享,很受用...

    回覆刪除
  8. 我有點困惑了,我寫出了一個看似A中有B,B中有A的程式
    但結果竟然可以編譯
    class ClassA
    {
    ClassB classb;
    public void MethodA()
    {
    Console.WriteLine("this is method A");
    }
    public void MethodA2()
    {
    classb = new ClassB();
    classb.MethodB();
    Console.WriteLine("this is the end");
    }
    }

    class ClassB
    {
    public ClassA classa;
    public void MethodB()
    {
    classa = new ClassA();
    classa.MethodA();
    Console.WriteLine("this is Method B");

    }
    }


    這樣呼叫A的methodA2是可運作的

    但是methodA2要實體化B

    而B內又有實體化A

    理論上會不能編譯

    可實際上運作順利,請問何解??

    回覆刪除
    回覆
    1. 因為你不是把產生物件寫在建構子當中
      所以編譯會過,只要不要呼叫那個函式,就不會產生實體
      如果你把產生實體都寫在建構子,那執行程式就會記憶體(資源)不足了

      刪除
  9. 好懷念吶,最近要重新熟悉c#委派的用法,找著找著,就找到這裡來了

    回覆刪除
  10. Lambda 不會很難,那個只是在程式語言中,加上一些特殊寫法而已
    只是沒有人講解,不好懂

    回覆刪除
  11. 不好意思仙人你的範例link已經失效了
    可以重新再po一次嗎><
    最近在學delegate 覺得您的文章淺顯易懂

    回覆刪除
    回覆

    1. 原來範例LINK失效了,我這幾天弄回來

      刪除
    2. LINE恢復了喔,可以下載範例了

      刪除
  12. oh~~我最近也在學delegate 也想看看範例 真心期待範例重出江湖
    看完後我對照我手邊的程式碼真的有點理解

    回覆刪除
    回覆
    1. 原來範例LINK失效了,我這幾天弄回來

      刪除
    2. LINE恢復了喔,可以下載範例了

      刪除
  13. 範例的連結無法使用,希望可以修復

    回覆刪除
  14. 範例的連結無法使用,希望可以修復
    另外希望C#文章能再多一點救救新手

    回覆刪除
  15. 這比微軟教的委派簡單多了=_= 非常感激
    另外範例的連結無法使用,希望可以修復

    回覆刪除
  16. 不好意思想詢問一下

    {
    .......什麼事情?就是要叫用那個ClassB的委派之前就行了。綁定的語法也是比較特殊,就是把委派物件跟函式綁在一起就是了。

    My_ClassB.DelegateObj = new ClassB.DelegateDefine( ClassA_Method );
    }

    理論上如果我要在CLASS A可以看到 CLASS B宣告的 DelegateObj,我必須要將DelegateObj 宣告成 static嗎?

    回覆刪除
  17. 範例的連結無法使用,希望可以修復

    回覆刪除
  18. 範例的連結無法使用,希望可以修復

    回覆刪除
  19. 受益良多,但是範例的連結無法使用了,希望您有空時能幫忙修復,以利造福更多人們,感謝您的教學與分享!

    回覆刪除