C#詳細講解委託_事件

1、委託》c/c++中的函數指針:函數指針》函數入口地址》函數操作
委託在c#中的不同就是面向對象,是引用類型(應用指定方法的地址),必須是先定義後實例化
1.1定義委託
delegate int someDelegate (int i,string str);//類似於抽象方法,之後方法頭,沒有方法體,等級與方法一級(定義時和方法同等定義)
1.2、實列化
someDelegate   sd=new someDelegate(某個方法名稱(參數、返回值要與委託定義時一致));
1.3、調用
sd(5,"hongying");//調用時向裏面傳入參數
*、函數的簽名一致,委託就是應用存儲爲函數的類型(就是方法作爲另一個方法的參數
委託定義:實際上委託就是一個,它定義方法的類型、返回值,使得可以將方法作爲另一個方法的參數來進行傳值,這種將方法動態的賦給參數的做法。
委託優點:1、可以避免在程序中大量使用if——else(switch)語句,來調用一個方法
                  2、同時使得程序具有更好地可擴展性
                  3、一個委託可同時綁定多個方法,當調用這個委託的時候,依次執行響應的方法
2、爲什麼使用委託:
2.1、我們來做一個例子,我用一個方法調用另一個方法,向某人問好,並且傳入姓名參數。
        static void GreatPeoson(string name)//向某人問好,傳遞某人的名字
        {
            EnglishSay(name);//方法中調用另一方法,並且傳入一個參數
        }
        static void EnglishSay(string name)//用英語向某人問好,傳遞某人的名字
        {
            Console.WriteLine("Good Morning  "+name);
        }
        static void Main(string[] args)
        {
            GreatPeoson("張勇寬");
            Console.ReadLine();
        }
2.1、現在我又想用中文向某人問好,當然前提我要知道使用哪一個語言去問好
首先定義一個枚舉類型的語言變量:
    enum Language
    { English, Chinese }

static void GreatPeoson(string name,Language lan)//向某人問好,傳遞某人的名字,並傳入用哪一個語言問好
        {
            switch (lan)
            {
                case Language.English:
                    EnglishSay(name);
                    break;
                case Language.Chinese:
                    ChineseSay(name);
                    break;
            }
        }
        static void EnglishSay(string name)//用英語向某人問好,傳遞某人的名字
        {
            Console.WriteLine("Good Morning  "+name);
        }
        static void ChineseSay(string name)//使用中文向某人問好
        {
            Console.WriteLine("你好,有什麼幫助嗎?"+name);
        }
        static void Main(string[] args)
        {
            GreatPeoson("Marry", Language.English);
            GreatPeoson("張勇寬",Language.Chinese);
            Console.ReadLine();
        }
說到這裏大家應該明白點了,如果我現在想用韓語、日語、法語、阿根廷語言對不同的人問好,那我就不停的要改枚舉類型、switch(if-else)語句,這樣做實在是太繁瑣了,很難實現系統的擴展性
2.2、我用委託(委託一個方法調用我指定的方法)來實現
       static void EnglishSay(string name)//用英語向某人問好,傳遞某人的名字
        {
            Console.WriteLine("Good Morning  "+name);
        }
        static void ChineseSay(string name)//使用中文向某人問好
        {
            Console.WriteLine("你好,有什麼幫助嗎?"+name);
        }
        delegate void EnglishSayDelegate(string name); //定義委託
        delegate void ChineseSayDelegate(string name);//定義用中文來說委託
        static void Main(string[] args)
        {
            EnglishSayDelegate en = new EnglishSayDelegate(EnglishSay);//初始化委託
            ChineseSayDelegate chinese = new ChineseSayDelegate(ChineseSay);
            en("Marry");//調用委託指定的方法
            chinese("紅鷹");
            Console.ReadLine();
        }
這樣就減少了許多if——else(switch)語句,並且使程序有了一個很好地擴展性。

2.2.1、委託的另一種初始化:EnglishSayDelegate en= EnglishSay;//直接把方法給委託對象
static void EnglishSay(string name)//用英語向某人問好,傳遞某人的名字
        {
            Console.WriteLine("Good Morning  " + name);
        }
        static void ChineseSay(string name)//使用中文向某人問好
        {
            Console.WriteLine("你好,有什麼幫助嗎?"+name);
        }
        delegate void EnglishSayDelegate(string name); //定義委託
        delegate void ChineseSayDelegate(string name);//定義用中文來說委託
        static void Main(string[] args)
        {
            string name1, name2;
            name1 = "Marry";
            name2 = "紅鷹";
            EnglishSayDelegate en= EnglishSay;//直接把方法給委託對象
            ChineseSayDelegate chi = ChineseSay;
            en(name1);//調用委託
            chi(name2);
            Console.ReadLine();
        }
2.3、同一個委託中也同時可以綁定多個方法(多個方法同時賦給同一個委託),調用這個委託的時候依次執行響應的方法。
       static void EnglishSay(string name)//用英語向某人問好,傳遞某人的名字
        {
            Console.WriteLine("Good Morning  " + name);
        }
        static void ChineseSay(string name)//使用中文向某人問好
        {
            Console.WriteLine("你好,有什麼幫助嗎?"+name);
        }
        static void hao(string name)
        {
            Console.WriteLine("你好<{0}",name);
        }
        delegate void EnglishSayDelegate(string name); //定義委託
        delegate void ChineseSayDelegate(string name);//定義用中文來說委託
        static void Main(string[] args)
        {
            string name1, name2;
            name1 = "Marry";
            name2 = "紅鷹";
            EnglishSayDelegate en= EnglishSay;//直接把方法給委託對象
            en += ChineseSay;
            en += hao;
            ChineseSayDelegate chi = ChineseSay;
            chi += EnglishSay;
            en(name1);//調用委託
            chi(name2);
            Console.ReadLine();
        }
注意:第一個“=”是賦值語句,第二個“+=”是綁定方法,如果直接寫“+=”,將出現委託對象沒有賦值的錯誤提示信息。
2.4、既然委託可以綁定多個方法,是否也可以取消對方法的綁定。答案是一定的。使用“-=”
 static void EnglishSay(string name)//用英語向某人問好,傳遞某人的名字
        {
            Console.WriteLine("Good Morning  " + name);
        }
        static void ChineseSay(string name)//使用中文向某人問好
        {
            Console.WriteLine("你好,有什麼幫助嗎?"+name);
        }
        static void hao(string name)
        {
            Console.WriteLine("你好<{0}",name);
        }
        delegate void EnglishSayDelegate(string name); //定義委託
        delegate void ChineseSayDelegate(string name);//定義用中文來說委託
        static void Main(string[] args)
        {
            string name1, name2;
            name1 = "Marry";
            name2 = "紅鷹";
            EnglishSayDelegate en= EnglishSay;//直接把方法給委託對象
            en += ChineseSay;
            en += hao;
            en -= ChineseSay;
            en -= hao;
            //en -= EnglishSay;//不可以取消所有的方法綁定,這樣會導致沒有實現對委託en的實列而出現編譯錯誤。
            ChineseSayDelegate chi = ChineseSay;
            chi += EnglishSay;
            en(name1);//調用委託
            chi(name2);
            Console.ReadLine();
        }
3、事件的由來
3.1、對於上面的介紹我們對委託有了詳細的理解,但是一般EnglishSay、ChineseSay方法一般都不在Program類中,而是在另一個類中,這樣我們做一下簡單的修改:
class SayManager 
    {
        public void EnglishSay(string name)//用英語向某人問好,傳遞某人的名字
        {
            Console.WriteLine("Good Morning  " + name);
        }
        public void ChineseSay(string name)//使用中文向某人問好
        {
            Console.WriteLine("你好,有什麼幫助嗎?" + name);
        }
        public void hao(string name)
        {
            Console.WriteLine("你好<{0}", name);
        }
    }
    class Program
    {
        delegate void EnglishSayDelegate(string name); //定義委託
        delegate void ChineseSayDelegate(string name);//定義用中文來說委託
        static void Main(string[] args)
        {
            string name1, name2;
            name1 = "Marry";
            name2 = "紅鷹";
            SayManager sm = new SayManager();//初始化類對象
            EnglishSayDelegate en= sm.EnglishSay;//直接把方法給委託對象
            en +=sm.ChineseSay;
            en += sm.hao;
            en -= sm.ChineseSay;
            ChineseSayDelegate chi = sm.ChineseSay;
            chi += sm.EnglishSay;
            en(name1);//調用委託
            chi(name2);
            Console.ReadLine();
        }
做到這裏我們還是感覺不怎麼好,因爲面向對象編程,就是對類進行封裝,那我們是不是可以把委託封裝到類裏面去呢?答案是肯定的。看下一個例子:
    //定義全局委託
    delegate void EnglishSayDelegate(string name); //定義委託
    delegate void ChineseSayDelegate(string name);//定義用中文來說委託

    class SayManager 
    {
        public  EnglishSayDelegate en;//在類內部聲明委託
        public void PersonToSay(string name)
        {
            if (en != null)
                en(name);//一般調用委託的方法樣式
        }
        public void EnglishSay(string name)//用英語向某人問好,傳遞某人的名字
        {
            Console.WriteLine("Good Morning  " + name);
        }
        public void ChineseSay(string name)//使用中文向某人問好
        {
            Console.WriteLine("你好,有什麼幫助嗎?" + name);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            string name1;
            name1 = "Marry";
            SayManager sm = new SayManager();
            sm.en= sm.EnglishSay;//直接把方法給委託對象,在這裏對委託賦值方法
            sm.en +=sm.ChineseSay;
            sm.PersonToSay(name1);//調用委託
            Console.ReadLine();
        }
我們看到這裏,感覺委託和string類型變量沒有什麼區別了,就是一個變量唄,你就可以對其進行一些設置、賦值。
  1. 我們看看上面類中委託,訪問修飾符public,這說明我們在外面可以對他隨意的賦值等操作,嚴重破壞了對象的封裝性,如果設置成了private,我們在外面怎麼訪問啊,更別說綁定事件了。
  2. 我們在對委託“=”和“+=”不是賦值就是綁定,都是對方法的調用,沒有什麼分別,這樣感覺讓人很彆扭。
  3. 我們想到了string類型的private字段,我們可以用屬性進行賦值、訪問,對於去別不大委託類型我們是不是也可以。這就到了Event出場了。event作用:封裝了委託類型的變量,事件只能通過“+=”或“-=”對事件綁定方法。
delegate void EnglishSayDelegate(string name); //定義委託
    class SayManager 
    {
        public event EnglishSayDelegate toSay;//在這裏聲明一個事件,與聲明委託基本相同,只是增加了一個event,
        //事件的定義 訪問修飾符   event  委託名   事件標示符
        public void PersonToSay(string name)
        {
            toSay(name);//調用事件
        }
        public void EnglishSay(string name)//用英語向某人問好,傳遞某人的名字
        {
            Console.WriteLine("Good Morning  " + name);
        }
        public void ChineseSay(string name)//使用中文向某人問好
        {
            Console.WriteLine("你好,有什麼幫助嗎?" + name);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            string name1;
            name1 = "Marry";
            SayManager sm = new SayManager();
            //sm.toSay = sm.EnglishSay;//編譯時錯誤:事件“LianxiDelegate.SayManager.toSay”只能出現在 += 或 -= 的左邊
            sm.toSay += sm.EnglishSay;
            sm.toSay += sm.ChineseSay;
            //sm.toSay -= sm.ChineseSay;
            sm.PersonToSay(name1);//調用委託
            Console.ReadLine();
        }
    }
4、委託、事件與Observer設計模式

範例說明

上面的例子已不足以再進行下面的講解了,我們來看一個新的範例,因爲之前已經介紹了很多的內容,所以本節的進度會稍微快一些:

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

現在我們需要寫個程序來模擬這個燒水的過程,我們將定義一個類來代表熱水器,我們管它叫: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度。
// 省略...

5、.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度。
// 省略 ...


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章