1. 程式人生 > >C#裡的委託和事件實現Observer(觀察者)

C#裡的委託和事件實現Observer(觀察者)

  一、委託的簡介
1、委託的宣告:
delegate  HandlerName ([parameters]) 

例如:
public delegate void PrintHandler(string str);  

委託宣告定義了一種型別,它用一組特定的引數以及返回型別來封裝方法。對於靜態方法,委託物件封裝要呼叫的方法。對於例項方法,委託物件同時封裝一個例項和該例項上的一個方法。如果您有一個委託物件和一組適當的引數,則可以用這些引數呼叫該委託。

2、委託的使用:

using System; 

public class MyClass 


   
public static void Main() 

   { 

      PrintStr myPrinter 
= new PrintStr(); 

      PrintHandler myHandler 
= null

      myHandler 
+= new PrintHandler(myPrinter.CallPrint); // 將委託連結到方法,來例項化委託 

    
if(myHandler!=null) {

         myHandler(
"Hello World!"); // 呼叫委託,相當於匿名呼叫委託所連結的方法 

         Console.Read(); 

   } 


public delegate void PrintHandler(string str); // 宣告委託型別 

public class PrintStr 


   
public void CallPrint(string input) 

   { 

   Console.WriteLine(input); 

   } 

在C#中使用委託方法:
· 建立委託所使用的方法必須和委託宣告相一致(引數列表、返回值都一致)
· 利用 +=、-=來進行委託的連結、取消連結或直接使用Delegate.Combine和Delegate.Remove方法來實現
· 可以使用MulticastDelegate的例項方法GetInvocationList()來獲取委託鏈中所有的委託
· 不能撰寫包含 out 引數的委託

二、事件的簡介
C# 中的"事件"是當物件發生某些事情時,類向該類的客戶提供通知的一種方法。
1、事件的宣告:
宣告的格式為:
event  EventName 

   因為使用委託來宣告事件,所以在類裡宣告事件時,首先必須先宣告該事件的委託型別(如果尚未宣告的話)。在上面我們已經提到過了委託型別的宣告,但是在.net framework下為事件使用的委託型別進行宣告時有更嚴格的規定:
(1)、 事件的委託型別應採用兩個引數;
(2)、兩個引數分別是:指示事件源的"物件源"引數和封裝事件的其他任何相關資訊的"e"引數;
(3)、"e"引數的型別應為EventArgs 類或派生自 EventArgs 類。

 

 上面的說法好像有誤!!!“事件的委託型別應採用兩個引數”???

 


如下的定義: 

public   delegate   void  PrintHandler( object  sender,System.EventArgs e);
然後我們才能宣告該委託型別的事件
例如:
public   event  PrintHandler Print;

注意:此時的Print即是 事件(event),又是 委託(PrintHandler)


當事件發生時,將呼叫其客戶提供給它的委託。

2、呼叫事件:

   類聲明瞭事件以後,可以就像處理所指示的委託型別的欄位那樣處理該事件。如果沒有任何客戶將委託與該事件繫結,則該欄位將為空;否則該欄位引用應在呼叫該事件時呼叫的委託。因此,呼叫事件時通常先檢查是否為空,然後再呼叫事件。(呼叫事件,即觸發事件,只能從宣告該事件的類內進行)

public   event  PrintHandler Print; 3、事件繫結:

   從類的外面來看,事件就象類的一個公共成員,通過 類名.事件名 的形式來訪問,但是隻能對它做繫結和解除繫結的操作,而不能有其他操作。
類名. Print  +=   new  PrintHandler(繫結的方法名)  //  將某個方法繫結到Print事件上 
類名. Print  -=   new  PrintHandler(繫結的方法名)  //  將某個已繫結到Print事件上的方法從Print事件上解除  三、委託和事件的使用
委託和事件在使用者介面程式裡用的比較的多,比如象在winform或webform的使用者UI上的button和它的click事件:
將Button1_Click()方法繫結到按鈕控制元件Button1的Click事件上 
this .Button1.Click  +=   new  System.EventHandler( this . Button1_Click);

private   void  Button1_Click( object  sender, System.EventArgs e)  //  Button1_Click()方法 


    
然而除了 使用者介面程式 外,在很多其他地方也用到了事件驅動模式,比如 觀察者模式(Observer) 釋出/訂閱(Publish/Subscribe) 裡:在一個類裡釋出(Publish)某個可以被觸發的事件,而其他的類就可以來訂閱(Subscribe)該事件。一旦這個釋出者類觸發了該事件,那麼執行時環境會立刻告知所有訂閱了該事件的訂閱者類:這個事件發生了!從而各個訂閱者類可以作出它們自己的反應(呼叫相應方法)。

我們來舉一個生活中的實際例子來說明如何使用委託和事件,以及使用委託和事件所帶來的好處:

比如說有一個公司(場景),你是老闆,手下有主管和員工,作為老闆你會指派(委託)主管管理員工的工作,如果某個員工玩遊戲,則讓某個主管從該員工的薪水裡扣去500元錢。

這就是現實中的委託。

而在寫程式中, 假設程式設計師就是老闆,有兩個類分別為主管和員工,而主管小王和員工小張就是兩個類的物件例項。員工類有一個方法:玩遊戲,同時就有一個玩遊戲的事件,他一玩遊戲就會激發這個事件。而主管類就是負責處理該事件的,他負責把玩遊戲的員工的薪水扣除500。

(一)、首先,我們來看看在非委託的情況下比較常見的一種設計方式(當然這不是唯一的方式,也不是最好的方式,但是很常見):
這種方法所帶來的問題: 員工類和主管類的耦合性太高
1、 在客戶程式裡必須先建立了主管類之後才能生成員工類,如果在不需要主管類物件而只需員工類物件的地方,為了建立所需的員工類物件例項,你也不得不去先建立一個主管類的物件例項;
2、 如果場景劇本(即客戶程式需求)發生了變化
(1)、現在要讓一個新的角色(一個新的類),如保安,來代替主管,負責在員工玩遊戲時扣員工薪水,那麼我們不得不去修改員工類,或許還需要修改主管類;
(2)、如果場景劇本增加新的需求,要求員工在玩遊戲後,不但要扣薪水,還要在績效上扣分,那麼我們也不得不修改員工類。
(二)、利用委託的實現:
using  System;
namespace  CSharpConsole
{

    
public   class  場景
    {

        [STAThread]

        
public   static   void  Main( string [] args)
        {

            Console.WriteLine(
" 場景開始了. " );

            
//  生成主管類的物件例項 小王 

            主管 小王 
=   new  主管();

            
//  生成員工類的物件例項 小張,指定他的主管 

            員工 小張 
=   new  員工(小王);



            Console.WriteLine(
" 該員工本有的薪水: "   +  小張.薪水.ToString());



            
//  員工開始玩遊戲 

            小張.玩遊戲();

            Console.WriteLine(
" 現在該員工還剩下: "   +  小張.薪水.ToString());



            Console.WriteLine(
" 場景結束 " );

            Console.ReadLine();

        }


     }

    // 負責扣錢的人----主管 

    
public class 主管
    {

        
public 主管()
        {

            Console.WriteLine(
"生成主管");

        }

        
public void 扣薪水(員工 employee)
        {

            Console.WriteLine(
"主管:好小子,上班時間膽敢玩遊戲");

            Console.WriteLine(
"主管:看看你小子有多少薪水");



            Console.WriteLine(
"開始扣薪水");

            System.Threading.Thread.Sleep(
1000);

            employee.薪水 
= employee.薪水 - 500;

            Console.WriteLine(
"扣薪水執行完畢.");

        }

    }

    
// 如果玩遊戲,則會引發事件 

    
public class 員工
    {

        
// 儲存員工的薪水 

        
private int m_Money;

        
// 儲存該員工的主管 

        
private 主管 m_Manager;

        
public 員工(主管 manager)
        {

            Console.WriteLine(
"生成員工.");

            m_Manager 
= manager;// 通過建構函式,初始化員工的主管。 

            m_Money 
= 1000;// 通過建構函式,初始化員工的薪水。 

        }

        
public int 薪水 // 此屬性可以操作員工的薪水 。 
        {

            
get
            {

                
return m_Money;

            }

            
set
            {

                m_Money 
= value;

            }

        }

        
public void 玩遊戲()
        {

            Console.WriteLine(
"員工開始玩遊戲了..");

            Console.WriteLine(
"員工:CS真好玩,哈哈哈! 我玩");

            System.Threading.Thread.Sleep(
1000);



            m_Manager.扣薪水(
this);

        }

    }

}

 

下面有個例子:在C# 控制檯應用程式編輯執行成功:
using System;
namespace CSharpConsole
{

    
// 定義委託 

    
public delegate void PlayGameHandler(object sender, System.EventArgs e);

    
// 負責扣錢的人----主管 

    
public class 主管
    {

        
public 主管()
        {

            Console.WriteLine(
"生成主管");

        }

        
public void 扣薪水(object sender, EventArgs e)
        {

            Console.WriteLine(
"主管:好小子,上班時間膽敢玩遊戲");

            Console.WriteLine(
"主管:看看你小子有多少薪水");



            員工 employee 
= (員工)sender;



            Console.WriteLine(
"開始扣薪水");

            System.Threading.Thread.Sleep(
1000);

            employee.薪水 
= employee.薪水 - 500;

            Console.WriteLine(
"扣薪水執行完畢.");

        }

    }

    
// 如果玩遊戲,則會引發事件 

    
public class 員工
    {

        
// 先定義一個事件,這個事件表示員工在玩遊戲。 

        
public event PlayGameHandler PlayGame;

        
// 儲存員工薪水的變數 

        
private int m_Money;

        
public 員工()
        {

            Console.WriteLine(
"生成員工.");

            m_Money 
= 1000// 建構函式,初始化員工的薪水。 

        }

        
public int 薪水 // 此屬性可以操作員工的薪水 。 
        {

            
get
            {

                
return m_Money;

            }

            
set
            {

                m_Money 
= value;

            }

        }

        
public void 玩遊戲()
        {

            Console.WriteLine(
"員工開始玩遊戲了..");

            Console.WriteLine(
"員工:CS真好玩,哈哈哈! 我玩");

            System.Threading.Thread.Sleep(
1000);

            System.EventArgs e 
= new EventArgs();

            OnPlayGame(e);

        }

        
protected virtual void OnPlayGame(EventArgs e)
        {

            
if (PlayGame != null)
            {

                PlayGame(
this, e);

            }

        }

    }

    
public class 場景
    {

        [STAThread]

        
public static void Main(string[] args)
        {

            Console.WriteLine(
"場景開始了.");

            
// 生成主管類的物件例項 小王 

            主管 小王 
= new 主管();

            
// 生成員工類的物件例項 小張 

            員工 小張 
= new 員工();

            
// 設下委託,指定監視 

            小張.PlayGame 
+= new PlayGameHandler(小王.扣薪水);



            Console.WriteLine(
"該員工本有的薪水:" + 小張.薪水.ToString());



            
// 員工開始玩遊戲 

            小張.玩遊戲();

            Console.WriteLine(
"現在該員工還剩下:" + 小張.薪水.ToString());



            Console.WriteLine(
"場景結束");

            Console.ReadLine();

        }

    }

}

對於前面提出的問題:
1、 解耦了主管類和員工類之間的必然聯絡,可以單獨建立員工類物件例項,而不用管是否有主管類物件例項的存在;
2、 在客戶程式需求變化時:
(1)、我們只需修改客戶程式,即上面例子裡的class 場景,將委託改為如下:

保安 小李  =   new  保安(); 
小張.PlayGame 
+=   new  PlayGameHandler(小李. 扣薪水); 即可實現由保安來負責扣薪水的需求變化,而不用動員工類。
(2)、我們只需修改客戶程式,即上面例子裡的class 場景,新增一個如下的委託:   

   
這個"某某"可以是主管,也可以是其他新的角色(新的類),只需要在"某某"對應的類裡定義扣績效分的動作即可,而不用動員工類。

四、總結:

   當然,不使用委託和事件我們仍然可以設計出解耦的類,然而卻會增加很多的類、介面以及關聯等等,增加了程式碼量和程式的邏輯複雜性,而在.net裡利用委託和事件我們只需少的多的程式碼來實現。

  委託和事件的使用有如下幾個要素:

1、激發事件的物件-----就是員工小張
2、處理物件事件的物件-----就是主管小王
3、定義委託,就是你讓主管小王監視員工小張。

如果這三個要素都滿足的話,則你就寫出了一個完整事件的處理。 分類: 03~c#