1. 程式人生 > 其它 >庖丁解牛——深入解析委託和事件

庖丁解牛——深入解析委託和事件

這篇博文我不講委託和事件的概念,因為大段的文字概念沒有任何意義。 具體想了解,委託和事件的概念可以MSDN查閱。 我這篇文章的主題思路是委託如何一步步進化成事件: 何為委託--->委託來實現事件--->用方法對委託的封裝--->Event的 add,remove方法的引入--->標準事件寫法--->反編譯探究竟。 用幾個例子以及Reflector反編譯探究委託和事件的關係。不足之處,還望多多指教...

何為委託:

首先,委託是一種型別,是一種定義了方法簽名的型別。

委託可以理解為函式指標(安全),並且委託約束了方法的簽名(由返回型別和引數組成),

所以例項化委託時,可以將其例項與任何具有相同簽名(由返回型別和引數組成)得方法相關聯,

如果按照C語言函式指標理解,即委託例項指向某個方法。

為什麼要用委託:

舉個簡單的例子:

例如,需要判斷一個數是為奇數還是偶數? 可能我們會這樣實現:


     static void Main(string[] args)
        {
            Console.WriteLine((4 % 2 == 0) ? "偶數" : "奇數");
            Console.WriteLine((5 % 2 == 0) ? "偶數" : "奇數");
            Console.ReadKey();
        }
}

上面例子很簡單,但是很不靈活,我們稍加改進:


static void Main(string[] args)
{
     Console.WriteLine("請輸入一個數字:");
     int i = int.Parse(Console.ReadLine());
     Console.WriteLine((i%2==0)?"偶數":"奇數");
     Console.ReadKey();
}

上面這個簡單的例子,也是挺有玄機的。對於程式設計師,我們不關心客戶端使用者傳過來是奇數還是偶數,況且我們也不知道傳過來的引數到底是多少, 我們只關心怎樣來實現功能。對於使用者來說,他們不必關心底層到底是怎樣實現功能的,他們只負責輸入數字即可“坐享其成”。這個例子, 可以理解成一個最簡單的解耦。

  看了上面這個例子,我們再舉一個例子來演示委託怎麼替做什麼:

    //委託是一種定義方法簽名的型別。 當例項化委託時,可以將其例項與任何具有相容簽名的方法相關聯。 可以通過委託例項呼叫方法。
    //一個委託宣告:
    public delegate void ChangeDelegate(int i);
    class Program
    {
        static void Main(string[] args)
        {
            ChangeDelegate c = null;
            if (DateTime.Now.Second%2==0)
            {
                c = Even;//委託可以理解為函式指標(安全),並且委託型別ChangeDelegate約束了引數型別,所以c可以指向Even方法
                c(DateTime.Now.Second);
            }
            else
            {
                //c = Odd;
                c = new ChangeDelegate(Odd);//標準寫法
                c(DateTime.Now.Second);
            }
            Console.ReadKey();
           
        }
        static void Even(int i)
        {
            Console.WriteLine("{0}是偶數",i);
        }
        static void Odd(int i)
        {
            Console.WriteLine("{0}是奇數",i);
         
        }
    }

上面程式碼可以看出,程式設計師並不知道委託例項到底指向那個函式,但他可以確定,指向的那個方法必定是受ChangeDelegate約束的。 再看一個例子,我們要對陣列進行操作:

 class Program
    {
        static void Main(string[] args)
        {
            int[] arr = { 1,2,3,4,5,6,7,8,9};
            List<int> newList=Filter(arr,IsOdd);
            Console.WriteLine(string.Join("-",newList.ToArray()));
            Console.ReadKey();

        }
        /// <summary>
        /// 判斷是否為偶數
        /// </summary>
        /// <param name="i"></param>
        /// <returns></returns>
        static bool IsEven(int i)
        { 
            return i%2==0;
        }
        /// <summary>
        /// 判斷是否為奇數
        /// </summary>
        /// <param name="i"></param>
        /// <returns></returns>
        static bool IsOdd(int i)
        { 
            return i%2==1;
            
        }
        
        static List<int> Filter(int[] value, FilterDelegate f)
        {
            List<int> list=new List<int> ();
            foreach (var item in value)
            {
                if (f(item))
                {
                    list.Add(item);
                }
            }
            return list;
        }
    }
    delegate bool FilterDelegate(int i);

 所以,static List<int> Filter(int[] value, FilterDelegate f),這兒只需要傳過來一個方法, 而我們程式設計師並不關心這是個什麼方法,能替我們做什麼,這就是委託的好處。

事件和委託的聯絡:學習事件之前,先來用委託來模擬實現事件:

    class Program
    {
        static void Main(string[] args)
        {
            Counter c = new Counter();
            c.onCount = Count;//相當於訂閱了一個事件
            c.onCount += Count_2;//可以多個人同時監聽了
            int j = 0;
            while (j<=100)
            {
                j++;
                c.Next();
            }
            Console.ReadKey();
        }
        static void Count(int value)
        {
            Console.WriteLine("Count監聽了{0}是偶數", value);
        }
        static void Count_2(int value)
        {
            Console.WriteLine("Count_2監聽了{0}是偶數", value);
        }
    }
    class Counter
    {
        public OnCountDelegate onCount;//把委託物件宣告為一個欄位
        private int i = 0;
        public void Next()
        {
            i++;
            if (i%2==0)
            {
                if (onCount!=null)
                {
                    //解耦:解除耦合。
                    //不用關心到底指向誰,呼叫就行
                    onCount(i);//觸發事件,呼叫onCount指向的函式,相當於把事件通知出去
                }
            }
        }
    }
    public delegate void OnCountDelegate(int value);
  c.onCount = Count;
  c.onCount += Count_2; 相當於監聽事件,觸發事件時,只要哦呼叫onCount指向的函式,這樣相當於把事件通知出去。所以上面這個Demo之後,我們再可以對委託來實現事件進行擴充套件: 我們自定義一個雙擊火箭手按鈕[使用者控制元件]: 
namespace 雙擊火箭手按鈕
{
    //宣告一個委託
    public delegate void DoubleClickDelegate();
    public partial class DoubleClickButton : UserControl
    {
        public DoubleClickButton()
        {
            InitializeComponent();
        }
       
        private int i;
        //把一個的委託物件定義為Public欄位
        public DoubleClickDelegate OnDoubleClick;

        private void button1_Click(object sender, EventArgs e)
        {

            i++;
            if (i==2)
            {
                i = 0;
                if (OnDoubleClick!=null)
                {
                    OnDoubleClick();//呼叫事件的響應函式
                }
            }
        }
    }
}

然後我們在Form1中託入一個我們自定義的DoubleClickButton:

這兒我們可以仿照普通Button按鈕監聽Click事件:  this.button1.Click += new System.EventHandler(this.button1_Click);來那樣做:對DoubleClickButton的

OnDoubleClick事件進行監聽:
        private void Form1_Load(object sender, EventArgs e)
        {
            doubleClickButton1.OnDoubleClick = doFire;
            doubleClickButton1.OnDoubleClick += doIce;        }
        void doFire()
        {
            MessageBox.Show("雙擊火槍手開火");
        }
           void doIce()
        {
            MessageBox.Show("雙擊火槍手下冰雨");
        }
這樣一個簡單的使用者控制元件就完成了,雙擊兩下觸發了OnDoubleClick事件,並且去執行相關聯的響應函式(doFire,doIce)。接著我們在對這個程式進行修改,來干擾我們的雙擊火槍手按鈕:

我們在搗亂這個按鈕的中寫入:

        private void button1_Click(object sender, EventArgs e)
        {
            doubleClickButton1.OnDoubleClick = null;
        }
上面的操作意味著我們把委託變數指向了NULL,這就破壞了之前我們的監聽事件。再比如:在模擬執行這個按鈕中寫入:
        private void button2_Click(object sender, EventArgs e)
        {
            doubleClickButton1.OnDoubleClick();
        }

上面程式碼模擬執行了雙擊火槍後按鈕,本來需要雙擊兩下才能觸發事件,而這兒可以直接去執行事件的響應函式。

所以為了防止外界對我的事件的干擾,我們把

public OnCountDelegate onCount;//把委託物件宣告為一個欄位

改為:

   //把委託申明為Private,防止外界直接=NULL或者OnDoubleClick()仿照事件
   private DoubleClickDelegate OnDoubleClick;

再對私有的委託用一個AddDoubleClick進行對外界的過濾,所以完整程式碼應該是這樣的:

 //把委託申明為Private,防止外界直接=NULL或者OnDoubleClick()仿照事件
        private DoubleClickDelegate _OnDoubleClick;
        public void AddDoubleClick(DoubleClickDelegate d)
        {
            _OnDoubleClick += d;
        }

        private void button1_Click(object sender, EventArgs e)
        {

            i++;
            if (i==2)
            {
                i = 0;
                if (_OnDoubleClick!=null)
                {
                    _OnDoubleClick();//呼叫事件的響應函式
                }
            }
        }

上面這樣處理之後,我們的控制元件就不會被外界干擾了。

但是這樣操作太複雜,所以微軟為我們提供了更為簡便的方式既 Event:

對上面的雙擊火槍手按鈕在做稍微修改:

namespace 三擊暴走按鈕
{
    public delegate void RampageDelegate();
    public partial class RampageThreeClickButton : UserControl
    {

        //定義一個私有的委託型別欄位
        private RampageDelegate onRampage;
        //類似於屬性那樣對委託進行了封裝
        public event RampageDelegate OnRampage 
        {
            add { onRampage += value; }
            remove { onRampage -= value; }
            
        }
        private int count;
        public RampageThreeClickButton()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            count++;
            if (count==3)
            {
                if (onRampage!=null)
                {
                    onRampage();
                }
                count = 0;
            }
        }
    }
}

再將定義好的暴走控制元件拖到Form1中:

後臺程式碼是這樣的:

       public Form1()
        {
            InitializeComponent();
        }

        private void rampageThreeClickButton1_Load(object sender, EventArgs e)
        {
            //對事件進行註冊,即監聽事件,只能用+=,不用用=,這樣防止了外界=NULL干擾
            rampageThreeClickButton1.OnRampage += doFire;
            //同理
            rampageThreeClickButton1.OnRampage += new RampageDelegate(doThunder);
            rampageThreeClickButton1.OnRampage += new RampageDelegate(doIce);

        }
        void doFire()
        {
            MessageBox.Show("烈焰之怒");
        }
        void doThunder()
        {
            MessageBox.Show("上古雷霆");
        }
        void doIce()
        {
            MessageBox.Show("極地冰雨");
        }

現在還能干擾嗎?當然不行:

OK,到目前為止,大家對委託和事件肯定有一個深刻的認識:

我們對事件進行反編譯之後,相信一切疑問皆是浮雲:

我們舉一個標準的事件案例:

namespace DoubleKissinUButton
{
    public delegate void KissinUDelegate();//委託別忘記
    public partial class KissinUButton : UserControl
    {
        public event KissinUDelegate onKissinU;//用Event關鍵字來申明一個事件
        public KissinUButton()
        {
            InitializeComponent();
        }
        private int i = 0;

        private void button1_Click(object sender, EventArgs e)
        {
            i++;
            if (i==2)
            {
                if (onKissinU!=null)
                {
                    onKissinU();
                }
            }
        }
    }
}

介面:

 public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            kissinUButton1.onKissinU += new KissinUDelegate(kissinUButton1_onKissinU);
        }

        void kissinUButton1_onKissinU()
        {
            MessageBox.Show("點我我就吻吻你");
        }

        private void Form1_Load(object sender, EventArgs e)
        {

        }
    }

通過反編譯看看:

點選進入看看:

委託和事件沒有可比性,因為委託是型別,事件是物件,上面說的是委託的物件(用委託方式實現的事件)和(標準的event方式實現)事件的區別。事件的內部是用委託實現的。 因為對於事件來講,外部只能“註冊自己+=、登出自己-=”,外界不可以登出其他的註冊者,外界不可以主動觸發事件,因此如果用Delegate就沒法進行上面的控制,因此誕生了事件這種語法。add、remove。 事件是用來閹割委託例項的。事件只能add、remove自己,不能賦值。事件只能+=、-=,不能=、不能外部觸發事件。