進階系列(7)——委託與事件

1、委託

(一) 引言html

       委託 和 事件在 .Net Framework中的應用很是普遍,然而,較好地理解委託和事件對不少接觸C#時間不長的人來講並不容易。它們就像是一道檻兒,過了這個檻的人,以爲真是太容易了,而沒有過去的人每次見到委託和事件就以爲內心別(biè)得慌,混身不自在。本文中,我將經過兩個範例由淺入深地講述什麼是委託、爲何要使用委託、事件的由來、.Net Framework中的委託和事件、委託和事件對Observer設計模式的意義,對它們的中間代碼也作了討論。設計模式

(二)將方法做爲方法的參數函數

       咱們先無論這個標題如何的繞口,也無論委託到底是個什麼東西,來看下面這兩個最簡單的方法,它們不過是在屏幕上輸出一句問候的話語:this

public void GetGreetByPeople(string name) 
{ // 作某些額外的事情,好比初始化之類,此處略 EnglishGreeting(name); } public void EnglishGreeting(string name)
{ Console.WriteLine(
"Morning, " + name); }

       暫且無論這兩個方法有沒有什麼實際意義。GetGreetByPeople用於向某人問好,當咱們傳遞表明某人姓名的name參數,好比說「Jimmy」,進去的時候,在這個方法中,將調用EnglishGreeting方法,再次傳遞name參數,EnglishGreeting則用於向屏幕輸出 「Morning, Jimmy」。編碼

       如今假設這個程序須要進行全球化,哎呀,很差了,我是中國人,我不明白「Morning」是什麼意思,怎麼辦呢?好吧,咱們再加個中文版的問候方法:spa

public void ChineseGreeting(string name)
{ 
Console.WriteLine("早上好, " + name);
}

      這時候,GetGreetByPeople也須要改一改了,否則如何判斷到底用哪一個版本的Greeting問候方法合適呢?在進行這個以前,咱們最好再定義一個枚舉做爲判斷的依據:翻譯

  public void GetGreetByPeople(Nation nation,string name)
        {
            switch (nation)
            {
                case Nation.China:
                    GetGreetByChina(name);
                    break;
                case Nation.English:
                    GetGreetByEnglish(name);
                    break;
                default:
                    break;
            }            
        }

         OK,儘管這樣解決了問題,但我不說你們也很容易想到,這個解決方案的可擴展性不好,若是往後咱們須要再添加韓文版、日文版,就不得不反覆修改枚舉和GetGreetByPeople()方法,以適應新的需求。設計

         在考慮新的解決方案以前,咱們先看看 GetGreetByPeople的方法簽名:code

public void GetGreetByPeople(Nation nation,string name)

       咱們僅看 string name,在這裏,string 是參數類型,name 是參數變量,當咱們賦給name字符串「jimmy」時,它就表明「jimmy」這個值;當咱們賦給它「張子陽」時,它又表明着「張子陽」這個值。而後,咱們能夠在方法體內對這個name進行其餘操做。哎,這簡直是廢話麼,剛學程序就知道了。orm

        若是你再仔細想一想,假如GetGreetByPeople()方法能夠接受一個參數變量,這個變量能夠表明另外一個方法,當咱們給這個變量賦值EnglishGreeting的時候,它表明着 EnglsihGreeting() 這個方法;當咱們給它賦值ChineseGreeting 的時候,它又表明ChineseGreeting()方法。咱們將這個參數變量命名爲 MakeGreeting,那麼不是能夠如同給name賦值時同樣,在調用 GetGreetByPeople()方法的時候,給這個MakeGreeting 參數也賦上值麼(ChineseGreeting或者EnglsihGreeting等)?而後,咱們在方法體內,也能夠像使用別的參數同樣使用MakeGreeting。可是,因爲MakeGreeting表明着一個方法,它的使用方式應該和它被賦的方法(好比ChineseGreeting)是同樣的,好比:

MakeGreeting(name);

好了,有了思路了,咱們如今就來改改GetGreetByPeople()方法,那麼它應該是這個樣子了:

public void GreetByPeople(string name, *** MakeGreeting){ MakeGreeting(name); }

       注意到 *** ,這個位置一般放置的應該是參數的類型,但到目前爲止,咱們僅僅是想到應該有個能夠表明方法的參數,並按這個思路去改寫GetGreetByPeople方法,如今就出現了一個大問題:這個表明着方法的MakeGreeting參數應該是什麼類型的?

      提示:這裏已再也不須要枚舉了,由於在給MakeGreeting賦值的時候動態地決定使用哪一個方法,是ChineseGreeting仍是 EnglishGreeting,而在這個兩個方法內部,已經對使用「morning」仍是「早上好」做了區分。

聰明的你應該已經想到了,如今是委託該出場的時候了,但講述委託以前,咱們再看看MakeGreeting參數所能表明的 ChineseGreeting()和EnglishGreeting()方法的簽名:

public void EnglishGreeting(string name) public void ChineseGreeting(string name)

      如同name能夠接受String類型的「true」和「1」,但不能接受bool類型的true和int類型的1同樣。MakeGreeting的參數類型定義應該可以肯定 MakeGreeting能夠表明的方法種類,再進一步講,就是MakeGreeting能夠表明的方法的參數類型和返回類型。因而,委託出現了:它定義了MakeGreeting參數所能表明的方法的種類,也就是MakeGreeting參數的類型。

     提示:若是上面這句話比較繞口,我把它翻譯成這樣:string 定義了name參數所能表明的值的種類,也就是name參數的類型。本例中委託的定義:

public delegate void GreetingDelegate(string name);

     能夠與上面EnglishGreeting()方法的簽名對比一下,除了加入了delegate關鍵字之外,其他的是否是徹底同樣?如今,讓咱們再次改動GreetPeople()方法,以下所示:

public void GreetPeople(string name, GreetingDelegate MakeGreeting){ MakeGreeting(name); }

   如你所見,委託GreetingDelegate出現的位置與 string相同,string是一個類型,那麼GreetingDelegate應該也是一個類型,或者叫類(Class)。可是委託的聲明方式和類卻徹底不一樣,這是怎麼一回事?實際上,委託在編譯的時候確實會編譯成類。由於Delegate是一個類,因此在任何能夠聲明類的地方均可以聲明委託。更多的內容將在下面講述,如今,請看看這個範例的完整代碼:

 public delegate void GetGeetByPeopleDelegate(string name);

    class Program
    {
        public static void GetGeetByEnglish(string name)
        {
            Console.WriteLine("GoodMoring:" + name);
        }

        public static void GetGreetByChina(string name)
        {
            Console.WriteLine("早上好:" + name);
        }


        public static void GetGreetByPeople(string name, GetGeetByPeopleDelegate getGeetByPeople)
        {
            getGeetByPeople(name);
        }

        static void Main(string[] args)
        {
            //GetGreetByPeople("Tom", GetGeetByEnglish);
            //GetGreetByPeople("張子陽", GetGreetByChina);
            //Console.ReadLine();
            GetGeetByPeopleDelegate g = new GetGeetByPeopleDelegate(GetGreetByChina);
            g += GetGeetByEnglish;
            GetGreetByPeople("tom",g);
            Console.ReadLine();
        }
    }

咱們如今對委託作一個總結:

     委託是一個類,它定義了方法的類型,使得能夠將方法看成另外一個方法的參數來進行傳遞,這種將方法動態地賦給參數的作法,能夠避免在程序中大量使用If-Else(Switch)語句,同時使得程序具備更好的可擴展性。

(三)、將方法綁定到委託

      看到這裏,是否是有那麼點如夢初醒的感受?因而,你是否是在想:在上面的例子中,我不必定要直接在GreetPeople()方法中給 name參數賦值,我能夠像這樣使用變量:

static void Main(string[] args) {
 string name1, name2; 
 name1 = "Jimmy Zhang"; name2 = "張子陽"; 
 GreetPeople(name1, EnglishGreeting); 
 GreetPeople(name2, ChineseGreeting);
 Console.ReadKey(); 
}

     而既然委託GreetingDelegate 和 類型 string 的地位同樣,都是定義了一種參數類型,那麼,我是否是也能夠這麼使用委託?

static void Main(string[] args) { 
GreetingDelegate delegate1, delegate2;
delegate1 = EnglishGreeting; delegate2 = ChineseGreeting; 
GreetPeople("Jimmy Zhang", delegate1); GreetPeople("張子陽", delegate2); Console.ReadKey(); }

     如你所料,這樣是沒有問題的,程序一如預料的那樣輸出。這裏,我想說的是委託不一樣於string的一個特性:能夠將多個方法賦給同一個委託,或者叫將多個方法綁定到同一個委託,當調用這個委託的時候,將依次調用其所綁定的方法。在這個例子中,語法以下:

static void Main(string[] args) {
GreetingDelegate delegate1; delegate1 = EnglishGreeting; // 先給委託類型的變量賦值 
delegate1 += ChineseGreeting; // 給此委託變量再綁定一個方法 // 將前後調用 EnglishGreeting 與 ChineseGreeting 方法
GreetPeople("Jimmy Zhang", delegate1); Console.ReadKey(); }

   輸出爲: Morning, Jimmy Zhang 早上好, Jimmy Zhang

   實際上,咱們能夠也能夠繞過GreetPeople方法,經過委託來直接調用EnglishGreeting和ChineseGreeting:

static void Main(string[] args) { 
GreetingDelegate delegate1; delegate1 = EnglishGreeting; // 先給委託類型的變量賦值 
delegate1 += ChineseGreeting; // 給此委託變量再綁定一個方法 // 將前後調用 EnglishGreeting 與 ChineseGreeting 方法
delegate1 ("Jimmy Zhang");
Console.ReadKey(); 
}

    注意:這在本例中是沒有問題的,但回頭看下上面GreetPeople()的定義,在它之中能夠作一些對於EnglshihGreeting和ChineseGreeting來講都須要進行的工做,爲了簡便我作了省略。

    注意這裏,第一次用的「=」,是賦值的語法;第二次,用的是「+=」,是綁定的語法。若是第一次就使用「+=」,將出現「使用了未賦值的局部變量」的編譯錯誤。

   咱們也可使用下面的代碼來這樣簡化這一過程:

GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting); 
delegate1 += ChineseGreeting; // 給此委託變量再綁定一個方法

     看到這裏,應該注意到,這段代碼第一條語句與實例化一個類是何其的類似,你不由想到:上面第一次綁定委託時不可使用「+=」的編譯錯誤,或許能夠用這樣的方法來避免:

 GreetingDelegate delegate1 = new GreetingDelegate();
delegate1 += EnglishGreeting; // 此次用的是 「+=」,綁定語法。 delegate1 += ChineseGreeting; // 給此委託變量再綁定一個方法

但實際上,這樣會出現編譯錯誤: 「GreetingDelegate」方法沒有采用「0」個參數的重載。儘管這樣的結果讓咱們以爲有點沮喪,可是編譯的提示:「沒有0個參數的重載」再次讓咱們聯想到了類的構造函數。我知道你必定按捺不住想探個究竟,但再此以前,咱們須要先把基礎知識和應用介紹完。

既然給委託能夠綁定一個方法,那麼也應該有辦法取消對方法的綁定,很容易想到,這個語法是「-=」:

static void Main(string[] args) {
GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting); delegate1 += ChineseGreeting; 
// 給此委託變量再綁定一個方法 // 將前後調用 EnglishGreeting 與 ChineseGreeting 方法 GreetPeople("Jimmy Zhang", delegate1); Console.WriteLine(); delegate1 -= EnglishGreeting; //取消對EnglishGreeting方法的綁定 // 將僅調用 ChineseGreeting GreetPeople("張子陽", delegate1); Console.ReadKey();
} //輸出爲: Morning, Jimmy Zhang 早上好, Jimmy Zhang 早上好, 張子陽

讓咱們再次對委託做個總結:

使用委託能夠將多個方法綁定到同一個委託變量,當調用此變量時(這裏用「調用」這個詞,是由於此變量表明一個方法),能夠依次調用全部綁定的方法。

(四)、Action和Func

Action 與 Func是.NET類庫中增長的內置委託,以便更加簡潔方便的使用委託。

Func

Func<TResult>  //無參數,有返回值
Func<T,TResult> //有一個參數,有返回值
Func<T1,T2,TResult> //有兩個參數,有返回值
Func<T1,T2,T3,TResult> //有三個參數,有返回值
Func<T1,T2,T3,T4,TResult //有四個參數,有返回值

它有5種形式,只是參數個數不一樣;參數的最多個數16個;

Action

Action<T>的用法與Func幾乎同樣,調用方法也相似。

Action //無參數,無返回值
Action<T>//有一個參數,無返回值
Action<T1,T2>//有兩個參數,無返回值
Action<T1,T2,T3>//有三個參數,無返回值
Action<T1,T2,T3,T4>//有四個參數,無返回值

Func與Action的區別

Func與Action做用幾乎同樣。只是Func<Result>有返回類型;
Action<T>只有參數類型,不能傳返回類型。
因此Action<T>的委託函數都是沒有返回值的。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace 委託的基礎
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }


        //一、聲明一個有參數,有返回值的方法
        Func<student, bool> filterDataDelegate = new Func<student, bool>(filterDataMethod);


        //二、聲明一個與委託有相同參數和返回值的方法
        public static bool filterDataMethod(student stu)
        {
            return stu.stuAge > 18;
        }


        //三、過濾方法的封裝,傳入一個委託,這個委託返回值是一個bool
        public List<student> ExectDataMethod(List<student> studetList, Func<student, bool> filterDataDelegate)
        {
            List<student> resultStudent = new List<student>();
            foreach (var item in studetList)
            {
                if (filterDataDelegate.Invoke(item))
                {
                    resultStudent.Add(item);
                }
            }
            return resultStudent;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            List<student> stuList = new List<student>();
            for (int i = 0; i < 16; i++)
            {
                student stu = new student()
                {
                    stuId = Convert.ToInt32(string.Format("1001{0}", i)),
                    stuName = string.Format("{0}號小明", i),
                    stuAge = 16 + i
                };
                stuList.Add(stu);
            }
            //四、執行過濾方法
            var result = ExectDataMethod(stuList, filterDataDelegate);
            var result2 = ExectDataMethod(stuList, x => x.stuAge > 19);
        }   
    }
}
2、事件

(一)、事件的由來

       咱們繼續思考上面的程序:上面的三個方法都定義在Programe類中,這樣作是爲了理解的方便,實際應用中,一般都是 GreetPeople 在一個類中,ChineseGreeting和 EnglishGreeting 在另外的類中。如今你已經對委託有了初步瞭解,是時候對上面的例子作個改進了。假設咱們將GreetingPeople()放在一個叫GreetingManager的類中,那麼新程序應該是這個樣子的:

namespace Delegate {
    //定義委託,它定義了能夠表明的方法的類型
    public delegate void GreetingDelegate(string name);
   
    //新建的GreetingManager類
    public class GreetingManager{
       public void GreetPeople(string name, GreetingDelegate MakeGreeting) {
           MakeGreeting(name);
       }
    }

    class Program {
       private static void EnglishGreeting(string name) {
           Console.WriteLine("Morning, " + name);
       }

       private static void ChineseGreeting(string name) {
           Console.WriteLine("早上好, " + name);
       }

       static void Main(string[] args) {
           // ... ...
        }
    }
}

這個時候,若是要實現前面演示的輸出效果,Main方法我想應該是這樣的:

static void Main(string[] args) {
    GreetingManager gm = new  GreetingManager();
    gm.GreetPeople("Jimmy Zhang", EnglishGreeting);
    gm.GreetPeople("張子陽", ChineseGreeting);
}

咱們運行這段代碼,嗯,沒有任何問題。程序一如預料地那樣輸出了:

Morning, Jimmy Zhang

早上好, 張子陽

如今,假設咱們須要使用上一節學到的知識,將多個方法綁定到同一個委託變量,該如何作呢?讓咱們再次改寫代碼:

static void Main(string[] args) {
    GreetingManager gm = new  GreetingManager();
    GreetingDelegate delegate1;
    delegate1 = EnglishGreeting;
    delegate1 += ChineseGreeting;

    gm.GreetPeople("Jimmy Zhang", delegate1);
}

輸出:
Morning, Jimmy Zhang
早上好, Jimmy Zhang

到了這裏,咱們不由想到:面向對象設計,講究的是對象的封裝,既然能夠聲明委託類型的變量(在上例中是delegate1),咱們何不將這個變量封裝到 GreetManager類中?在這個類的客戶端中使用不是更方便麼?因而,咱們改寫GreetManager類,像這樣:

public class GreetingManager{
    //在GreetingManager類的內部聲明delegate1變量
    public GreetingDelegate delegate1; 

    public void GreetPeople(string name, GreetingDelegate MakeGreeting) {
       MakeGreeting(name);
    }
 

如今,咱們能夠這樣使用這個委託變量:

static void Main(string[] args) {
    GreetingManager gm = new  GreetingManager();
    gm.delegate1 = EnglishGreeting;
    gm.delegate1 += ChineseGreeting;

    gm.GreetPeople("Jimmy Zhang", gm.delegate1);
}

輸出爲:
Morning, Jimmy Zhang
早上好, Jimmy Zhang

儘管這樣作沒有任何問題,但咱們發現這條語句很奇怪。在調用gm.GreetPeople方法的時候,再次傳遞了gm的delegate1字段:

gm.GreetPeople("Jimmy Zhang", gm.delegate1);

既然如此,咱們何不修改 GreetingManager 類成這樣:

public class GreetingManager{
    //在GreetingManager類的內部聲明delegate1變量
    public GreetingDelegate delegate1; 

    public void GreetPeople(string name) {
        if(delegate1!=null){     //若是有方法註冊委託變量
          delegate1(name);      //經過委託調用方法
       }
    }
}

在客戶端,調用看上去更簡潔一些:

static void Main(string[] args) {
    GreetingManager gm = new  GreetingManager();
    gm.delegate1 = EnglishGreeting;
    gm.delegate1 += ChineseGreeting;

    gm.GreetPeople("Jimmy Zhang");      //注意,此次不須要再傳遞 delegate1變量
}

輸出爲:
Morning, Jimmy Zhang
早上好, Jimmy Zhang

儘管這樣達到了咱們要的效果,可是仍是存在着問題:

在這裏,delegate1和咱們平時用的string類型的變量沒有什麼分別,而咱們知道,並非全部的字段都應該聲明成public,合適的作法是應該public的時候public,應該private的時候private。

咱們先看看若是把 delegate1 聲明爲 private會怎樣?結果就是:這簡直就是在搞笑。由於聲明委託的目的就是爲了把它暴露在類的客戶端進行方法的註冊,你把它聲明爲private了,客戶端對它根本就不可見,那它還有什麼用?

再看看把delegate1 聲明爲 public 會怎樣?結果就是:在客戶端能夠對它進行隨意的賦值等操做,嚴重破壞對象的封裝性。

最後,第一個方法註冊用「=」,是賦值語法,由於要進行實例化,第二個方法註冊則用的是「+=」。可是,不管是賦值仍是註冊,都是將方法綁定到委託上,除了調用時前後順序不一樣,再沒有任何的分別,這樣不是讓人以爲很彆扭麼?

如今咱們想一想,若是delegate1不是一個委託類型,而是一個string類型,你會怎麼作?答案是使用屬性對字段進行封裝。

因而,Event出場了,它封裝了委託類型的變量,使得:在類的內部,無論你聲明它是public仍是protected,它老是private的。在類的外部,註冊「+=」和註銷「-=」的訪問限定符與你在聲明事件時使用的訪問符相同。

咱們改寫GreetingManager類,它變成了這個樣子:

public class GreetingManager{
    //這一次咱們在這裏聲明一個事件
    public event GreetingDelegate MakeGreet;

    public void GreetPeople(string name) {
        MakeGreet(name);
    }
}

很容易注意到:MakeGreet 事件的聲明與以前委託變量delegate1的聲明惟一的區別是多了一個event關鍵字。看到這裏,在結合上面的講解,你應該明白到:事件其實沒什麼很差理解的,聲明一個事件不過相似於聲明一個進行了封裝的委託類型的變量而已。

爲了證實上面的推論,若是咱們像下面這樣改寫Main方法:

static void Main(string[] args) {
    GreetingManager gm = new  GreetingManager();
    gm.MakeGreet = EnglishGreeting;         // 編譯錯誤1
    gm.MakeGreet += ChineseGreeting;

    gm.GreetPeople("Jimmy Zhang");
}

會獲得編譯錯誤:事件「Delegate.GreetingManager.MakeGreet」只能出如今 += 或 -= 的左邊(從類型「Delegate.GreetingManager」中使用時除外)。

(二)、事件和委託的編譯代碼

這時候,咱們註釋掉編譯錯誤的行,而後從新進行編譯,再借助Reflactor來對 event的聲明語句作一探究,看看爲何會發生這樣的錯誤:

public event GreetingDelegate MakeGreet;

能夠看到,實際上儘管咱們在GreetingManager裏將 MakeGreet 聲明爲public,可是,實際上MakeGreet會被編譯成 私有字段,難怪會發生上面的編譯錯誤了,由於它根本就不容許在GreetingManager類的外面以賦值的方式訪問,從而驗證了咱們上面所作的推論。

咱們再進一步看下MakeGreet所產生的代碼:

private GreetingDelegate MakeGreet; //對事件的聲明 實際是 聲明一個私有的委託變量
 
[MethodImpl(MethodImplOptions.Synchronized)]
public void add_MakeGreet(GreetingDelegate value){
    this.MakeGreet = (GreetingDelegate) Delegate.Combine(this.MakeGreet, value);
}

[MethodImpl(MethodImplOptions.Synchronized)]
public void remove_MakeGreet(GreetingDelegate value){
    this.MakeGreet = (GreetingDelegate) Delegate.Remove(this.MakeGreet, value);
}

如今已經很明確了:MakeGreet事件確實是一個GreetingDelegate類型的委託,只不過無論是否是聲明爲public,它老是被聲明爲private。另外,它還有兩個方法,分別是add_MakeGreet和remove_MakeGreet,這兩個方法分別用於註冊委託類型的方法和取消註冊。實際上也就是: 「+= 」對應 add_MakeGreet,「-=」對應remove_MakeGreet。而這兩個方法的訪問限制取決於聲明事件時的訪問限制符。

在add_MakeGreet()方法內部,實際上調用了System.Delegate的Combine()靜態方法,這個方法用於將當前的變量添加到委託鏈表中。咱們前面提到過兩次,說委託其實是一個類,在咱們定義委託的時候:

public delegate void GreetingDelegate(string name);

當編譯器遇到這段代碼的時候,會生成下面這樣一個完整的類:

public sealed class GreetingDelegate:System.MulticastDelegate{
    public GreetingDelegate(object @object, IntPtr method);
    public virtual IAsyncResult BeginInvoke(string name, AsyncCallback callback, object @object);
    public virtual void EndInvoke(IAsyncResult result);
    public virtual void Invoke(string name);
}

關於這個類的更深刻內容,能夠參閱《CLR Via C#》等相關書籍,這裏就再也不討論了。

(三)、委託和事件與Observer設計模式

範例說明

上面的例子已不足以再進行下面的講解了,咱們來看一個新的範例,由於以前已經介紹了不少的內容,因此本節的進度會稍微快一些:

 假設咱們有個高檔的熱水器,咱們給它通上電,當水溫超過95度的時候:一、揚聲器會開始發出語音,告訴你水的溫度;二、液晶屏也會改變水溫的顯示,來提示水已經快燒開了。

如今咱們須要寫個程序來模擬這個燒水的過程,咱們將定義一個類來表明熱水器,咱們管它叫:Heater,它有表明水溫的字段,叫作temperature;固然,還有必不可少的給水加熱方法BoilWater(),一個發出語音警報的方法MakeAlert(),一個顯示水溫的方法,ShowMsg()。

namespace Delegate {
    class Heater {
    private int temperature; // 水溫
    // 燒水
    public void BoilWater() {
        for (int i = 0; i <= 100; i++) {
           temperature = i;

           if (temperature > 95) {
               MakeAlert(temperature);
               ShowMsg(temperature);
            }
        }
    }

    // 發出語音警報
    private void MakeAlert(int param) {
       Console.WriteLine("Alarm:嘀嘀嘀,水已經 {0} 度了:" , param);
    }
   
    // 顯示水溫
    private void ShowMsg(int param) {
       Console.WriteLine("Display:水快開了,當前溫度:{0}度。" , param);
    }
}

class Program {
    static void Main() {
       Heater ht = new Heater();
       ht.BoilWater();
    }
}
}

(四)、Observer設計模式簡介

上面的例子顯然能完成咱們以前描述的工做,可是卻並不夠好。如今假設熱水器由三部分組成:熱水器、警報器、顯示器,它們來自於不一樣廠商並進行了組裝。那麼,應該是熱水器僅僅負責燒水,它不能發出警報也不能顯示水溫;在水燒開時由警報器發出警報、顯示器顯示提示和水溫。

這時候,上面的例子就應該變成這個樣子:   

// 熱水器
public class Heater {
    private int temperature;
       
    // 燒水
    private void BoilWater() {
       for (int i = 0; i <= 100; i++) {
           temperature = i;
        }
    }
}

// 警報器
public class Alarm{
    private void MakeAlert(int param) {
       Console.WriteLine("Alarm:嘀嘀嘀,水已經 {0} 度了:" , param);
    }
}

// 顯示器
public class Display{
    private void ShowMsg(int param) {
       Console.WriteLine("Display:水已燒開,當前溫度:{0}度。" , param);
    }
}

這裏就出現了一個問題:如何在水燒開的時候通知報警器和顯示器?在繼續進行以前,咱們先了解一下Observer設計模式,Observer設計模式中主要包括以下兩類對象:

  1. Subject:監視對象,它每每包含着其餘對象所感興趣的內容。在本範例中,熱水器就是一個監視對象,它包含的其餘對象所感興趣的內容,就是temprature字段,當這個字段的值快到100時,會不斷把數據發給監視它的對象。
  2. Observer:監視者,它監視Subject,當Subject中的某件事發生的時候,會告知Observer,而Observer則會採起相應的行動。在本範例中,Observer有警報器和顯示器,它們採起的行動分別是發出警報和顯示水溫。

在本例中,事情發生的順序應該是這樣的:

  1. 警報器和顯示器告訴熱水器,它對它的溫度比較感興趣(註冊)。
  2. 熱水器知道後保留對警報器和顯示器的引用。
  3. 熱水器進行燒水這一動做,當水溫超過95度時,經過對警報器和顯示器的引用,自動調用警報器的MakeAlert()方法、顯示器的ShowMsg()方法。

相似這樣的例子是不少的,GOF對它進行了抽象,稱爲Observer設計模式:Observer設計模式是爲了定義對象間的一種一對多的依賴關係,以便於當一個對象的狀態改變時,其餘依賴於它的對象會被自動告知並更新。Observer模式是一種鬆耦合的設計模式。

實現範例的Observer設計模式

咱們以前已經對委託和事件介紹不少了,如今寫代碼應該很容易了,如今在這裏直接給出代碼,並在註釋中加以說明。

using System;
using System.Collections.Generic;
using System.Text;

namespace Delegate {
    // 熱水器
    public class Heater {
       private int temperature;
       public delegate void BoilHandler(int param);   //聲明委託
       public event BoilHandler BoilEvent;        //聲明事件

       // 燒水
       public void BoilWater() {
           for (int i = 0; i <= 100; i++) {
              temperature = i;

              if (temperature > 95) {
                  if (BoilEvent != null) { //若是有對象註冊
                      BoilEvent(temperature);  //調用全部註冊對象的方法
                  }
              }
           }
       }
    }

    // 警報器
    public class Alarm {
       public void MakeAlert(int param) {
           Console.WriteLine("Alarm:嘀嘀嘀,水已經 {0} 度了:", param);
       }
    }

    // 顯示器
    public class Display {
       public static void ShowMsg(int param) { //靜態方法
           Console.WriteLine("Display:水快燒開了,當前溫度:{0}度。", param);
       }
    }
   
    class Program {
       static void Main() {
           Heater heater = new Heater();
           Alarm alarm = new Alarm();

           heater.BoilEvent += alarm.MakeAlert;    //註冊方法
           heater.BoilEvent += (new Alarm()).MakeAlert;   //給匿名對象註冊方法
           heater.BoilEvent += Display.ShowMsg;       //註冊靜態方法

           heater.BoilWater();   //燒水,會自動調用註冊過對象的方法
       }
    }
}

輸出爲:
Alarm:嘀嘀嘀,水已經 96 度了:
Alarm:嘀嘀嘀,水已經 96 度了:
Display:水快燒開了,當前溫度:96度。
// 省略...

 (五)、Net Framework中的委託與事件

儘管上面的範例很好地完成了咱們想要完成的工做,可是咱們不只疑惑:爲何.Net Framework 中的事件模型和上面的不一樣?爲何有不少的EventArgs參數?

在回答上面的問題以前,咱們先搞懂 .Net Framework的編碼規範:

  • 委託類型的名稱都應該以EventHandler結束。
  • 委託的原型定義:有一個void返回值,並接受兩個輸入參數:一個Object 類型,一個 EventArgs類型(或繼承自EventArgs)。
  • 事件的命名爲 委託去掉 EventHandler以後剩餘的部分。
  • 繼承自EventArgs的類型應該以EventArgs結尾。

再作一下說明:

  1. 委託聲明原型中的Object類型的參數表明了Subject,也就是監視對象,在本例中是 Heater(熱水器)。回調函數(好比Alarm的MakeAlert)能夠經過它訪問觸發事件的對象(Heater)。
  2. EventArgs 對象包含了Observer所感興趣的數據,在本例中是temperature。

上面這些其實不只僅是爲了編碼規範而已,這樣也使得程序有更大的靈活性。好比說,若是咱們不光想得到熱水器的溫度,還想在Observer端(警報器或者顯示器)方法中得到它的生產日期、型號、價格,那麼委託和方法的聲明都會變得很麻煩,而若是咱們將熱水器的引用傳給警報器的方法,就能夠在方法中直接訪問熱水器了。

如今咱們改寫以前的範例,讓它符合 .Net Framework 的規範:

using System;
using System.Collections.Generic;
using System.Text;

namespace Delegate {
    // 熱水器
    public class Heater {
       private int temperature;
       public string type = "RealFire 001";       // 添加型號做爲演示
       public string area = "China Xian";         // 添加產地做爲演示
       //聲明委託
       public delegate void BoiledEventHandler(Object sender, BoiledEventArgs e);
       public event BoiledEventHandler Boiled; //聲明事件

       // 定義BoiledEventArgs類,傳遞給Observer所感興趣的信息
       public class BoiledEventArgs : EventArgs {
           public readonly int temperature;
           public BoiledEventArgs(int temperature) {
              this.temperature = temperature;
           }
       }

       // 能夠供繼承自 Heater 的類重寫,以便繼承類拒絕其餘對象對它的監視
       protected virtual void OnBoiled(BoiledEventArgs e) {
           if (Boiled != null) { // 若是有對象註冊
              Boiled(this, e);  // 調用全部註冊對象的方法
           }
       }
      
       // 燒水。
       public void BoilWater() {
           for (int i = 0; i <= 100; i++) {
              temperature = i;
              if (temperature > 95) {
                  //創建BoiledEventArgs 對象。
                  BoiledEventArgs e = new BoiledEventArgs(temperature);
                  OnBoiled(e);  // 調用 OnBolied方法
              }
           }
       }
    }

    // 警報器
    public class Alarm {
       public void MakeAlert(Object sender, Heater.BoiledEventArgs e) {
           Heater heater = (Heater)sender;     //這裏是否是很熟悉呢?
           //訪問 sender 中的公共字段
           Console.WriteLine("Alarm:{0} - {1}: ", heater.area, heater.type);
           Console.WriteLine("Alarm: 嘀嘀嘀,水已經 {0} 度了:", e.temperature);
           Console.WriteLine();
       }
    }

    // 顯示器
    public class Display {
       public static void ShowMsg(Object sender, Heater.BoiledEventArgs e) {   //靜態方法
           Heater heater = (Heater)sender;
           Console.WriteLine("Display:{0} - {1}: ", heater.area, heater.type);
           Console.WriteLine("Display:水快燒開了,當前溫度:{0}度。", e.temperature);
           Console.WriteLine();
       }
    }

    class Program {
       static void Main() {
           Heater heater = new Heater();
           Alarm alarm = new Alarm();

           heater.Boiled += alarm.MakeAlert;   //註冊方法
           heater.Boiled += (new Alarm()).MakeAlert;      //給匿名對象註冊方法
           heater.Boiled += new Heater.BoiledEventHandler(alarm.MakeAlert);    //也能夠這麼註冊
           heater.Boiled += Display.ShowMsg;       //註冊靜態方法

           heater.BoilWater();   //燒水,會自動調用註冊過對象的方法
       }
    }
}

輸出爲:
Alarm:China Xian - RealFire 001:
Alarm: 嘀嘀嘀,水已經 96 度了:
Alarm:China Xian - RealFire 001:
Alarm: 嘀嘀嘀,水已經 96 度了:
Alarm:China Xian - RealFire 001:
Alarm: 嘀嘀嘀,水已經 96 度了:
Display:China Xian - RealFire 001:
Display:水快燒開了,當前溫度:96度。
// 省略 ...

參考資料:《NET之美》、http://www.cnblogs.com/JimmyZhang/archive/2007/09/23/903360.html

相關文章
相關標籤/搜索