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 不可以