深入淺出話事件

小序

         在上篇文章(《深入淺出話委託》)中,我們集中討論了什麼是委託以及委託的用法。有朋友問:什麼時候用委託——說實話,使用某種編程要素是一種思想,更是一種習慣。舉個極端點的例子:比如你問我“什麼時候使用for循環”,我完全可以回答——根本用不着for循環,用ifgoto就完全能夠搞定——我們大多數人使用for循環,是因爲我們認同for循環的思想,並且養成了使用for循環的習慣。委託也是這樣——沒有委託的日子,程序員們一樣在幹活,只是有了委託機制後,大家幹起來更方便、寫出的代碼質量更高——當你體驗到它的方便、自然而然地使用它、養成一種習慣後,你就知道什麼時候應該使用它了。OK,我們回到正題上來,委託最常用的一個領域是用來聲明“事件”,或者說:委託是事件的基礎。作爲《深入淺出話委託》的姊妹篇,本文我們主要來討論事件(Event)。

正文

一.什麼是事件

         程序語言是對人類語言的抽象,因此,程序語言要素往往與人類語言中對應的詞彙有着相近的含義。正是這種“相近”,讓很多要素看上去很好懂,但如果不仔細理解,就會誤入歧途。“事件”就是這類要素中的一個,讓我們小心對待。

現實世界中的事件

         先讓我們看一看現實世界中的“事件”。

         現實世界中的“事件”(Event)是指“有一定社會意義或影響的大事情”。 提取一下句子的二級主幹,我們可以得出:事件=有意義或影響的事情。因此,我們可以看出,判定是否爲一個“事件”有兩個先決條件:首先它要是一個“事情”,然後這個事情還要“有意義或影響”。

         接着,我們進一步分析“事情”這個概念。我們常說“一件事情發生了”,這個“發生”組成要素又無外乎時間、地點、參與人物(主體)、所涉及的客體——抽象一點,我們可以把這些要素稱爲“事情”的參數

         一件事情發生了,可能對某些客體(Client)產生影響,也可能沒有任何影響。如果事情發生了、並對客體產生了影響,這時候,我們就應該拿出影響這一影響的辦法來

舉個例子:大樓的火警響了(火警鳴響這一事件發生),它產生的影響是讓樓內的所有人員都聽到了警報聲,樓內的人紛紛拿出自己響應這一影響的方法來——普通職員們飛奔出大樓,而消防人員卻向相反的方向跑,衝向最危險的火場。我們把這種套路稱爲“事件響應機制”,用於響應事件所造成的影響而採取的行動簡稱爲“事件響應方法”。特別注意:員工逃跑和消防員衝向火場都是對警號鳴響這一事件的響應方法,而非事件所產生的影響。

對了,還有個小問題:火警響了,我們爲什麼會跑呢?呵呵,答案很簡單——因爲我們時刻關心着警報會不會響這個事件

         OK,非常感謝你能把上面的文字讀完——初中語文老師的水平完全可以決定一個學生以後是不是能成爲一名合格的程序員。

.NET Framework 中事件的概念

         下面讓我們再來看看C#中“事件”(Event)是什麼,並且是如何與現實世界中的事件概念相對應。

1.          MSDNevent關鍵字的解釋:
Events are used on classes and structs to notify objects of occurrences that may affect their state.
事件被用在類和結構體上,用處是通知某些對象——這些對象的狀態有可能被事件的發生所影響。

2.          MSDNEvent的解釋:
An event is a way for a class to provide notifications when something of interest happens. 
事件,是當某些被關注的事情發生時類提供通知的一種途徑。

3.          C# Spec中對Event成員的解釋:
An event is a member that enables an object or class to provide notifications. Clients can attach executable code for events by supplying event handlers.
事件是一種類成員,它使得對象或類能夠提供通知。客戶端(被通知的對象/類)可以爲事件附加上一些可執行代碼來響應事件,這些可執行代碼稱爲“事件處理器”(Event Handlers)。

4.          我自己的解釋:
Events is a kind of member of class and structs, and it is a way for class and structs who own the events to notify objects who cares these events and provides the event handlers when these events fire.

         事件是類和結構體的一種成員。當事件發生時,“事件”是一種擁有此事件的類/結構體通知關心(或者稱爲“訂閱”)這些事件、並提供事件處理器的對象的一種途徑。

 

 

幹說沒啥意思,下面我還是給出相應的代碼,帶領大家體驗一下什麼是事件、事件是怎麼聲明的、事件如何被其它類“訂閱”、訂閱了事件的類又是如何響應(處理)事件的。

 

我們就以大樓火警爲例,給出下面的代碼:

//=============水之真諦============
//
//        http://blog.csdn.net/FantasiaX
//
//=========
上善若水,潤物無聲==========
using System;
using System.Collections.Generic;
using System.Text;

namespace EventSample
{
    // 委託是事件的基礎,是通知的發送方與接收方雙方共同遵守的"約定"
    delegate void FireAlarmDelegate();

    // 大樓(類)
    class Building
    {
        // 聲明事件:事件以委託爲基礎
        public event FireAlarmDelegate FireAlarmRing;
        
        //大樓失火,引發火警鳴響事件
        public void OnFire()
        {
            this.FireAlarmRing();
        }
    }

    // 員工(類)
    class Employee
    {
        // 這是員工對火警事件的響應,即員工的Event handler。注意與委託的匹配。
        public void RunAway()
        {
            Console.WriteLine("Running awary...");
        }
    }

    // 消防員(類)
    class Fireman
    {
        // 這是消防員對火警事件的響應,即消防員的Event handler。注意與委託的匹配。
        public void RushIntoFire()
        {
            Console.WriteLine("Fighting with fire...");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Building sigma = new Building();
            Employee employee = new Employee();
            Fireman fireman = new Fireman();

            // 事件的影響者"訂閱"事件,開始關心這個事件發生沒發生
            sigma.FireAlarmRing+=new FireAlarmDelegate(employee.RunAway);
            sigma.FireAlarmRing += new FireAlarmDelegate(fireman.RushIntoFire);

            //由你來放火!
            Console.WriteLine("Please input 'FIRE' to fire the building...");
            string str = Console.ReadLine();
            if (str.ToUpper()=="FIRE")
            {
                sigma. OnFire();
            }
        }
    }
}
      上面的代碼中提到:事件是基於委託的,委託不但是聲明事件的基礎,同時也是通知收發雙方必需共同遵守的一個“約定”。OK,讓我們改進一下上面的例子,進一步發揮事件的威力。

      設想這樣一個情況:大樓一共是7層,每層的防火做的也不錯,只要不是火特別大那麼就沒必要讓所有人都撤離——哪層着火,哪層員工撤離。還有就是一個火警的級別問題:我們把火的大小分爲三級——

C級(小火):打火機級,我左邊的兄弟比較喜歡抽菸,一般他點菸的時候我不跑。

B級(中火):比較大了,要求所在樓層的人員撤離。

A級(大火):一般女友發脾氣都是這個級別,要求全樓人撤離。

OK,讓我們看看代碼:

//=============水之真諦============
//
//        http://blog.csdn.net/FantasiaX
//
//=========
上善若水,潤物無聲==========
using System;
using System.Collections.Generic;
using System.Text;

namespace EventSample
{
    // 事件參數類:記載着火的樓層和級別
    class FireEventArgs
    {
        public int floor;
        public char fireLevel;
    }

    // 委託是事件的基礎,是通知的發送方與接收方共同遵守的"約定"
    delegate void FireAlarmDelegate(object sender, FireEventArgs e);

    // 大樓(類)
    class Building
    {
        // 聲明事件:事件以委託爲基礎
        public event FireAlarmDelegate FireAlarmRing;

        //大樓失火,引發火警鳴響事件
        public void OnFire(int floor, char level)
        {
            FireEventArgs e = new FireEventArgs();
            e.floor = floor;
            e.fireLevel = level;
            this.FireAlarmRing(this, e);
        }
        public string buildingName;
    }

    // 員工(類)
    class Employee
    {
        public string workingPlace;
        public int workingFloor;
        // 這是員工對火警事件的響應,即員工的Event handler。注意與委託的匹配。
        public void RunAway(object sender, FireEventArgs e)
        {
            Building firePlace = (Building)sender;
            if (firePlace.buildingName == this.workingPlace && (e.fireLevel == 'A' || e.floor ==this.workingFloor))
            {
                Console.WriteLine("Running awary...");
            }
        }
    }

    // 消防員(類)
    class Fireman
    {
        // 這是消防員對火警事件的響應,即消防員的Event handler。注意與委託的匹配。
        public void RushIntoFire(object sender, FireEventArgs e)
        {
            Console.WriteLine("Fighting with fire...");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Building sigma = new Building();
            Employee employee = new Employee();
            Fireman fireman = new Fireman();

            sigma.buildingName = "Sigma";
            employee.workingPlace = "Sigma";
            employee.workingFloor = 1;

            // 事件的影響者"訂閱"事件,開始關心這個事件發生沒發生
            sigma.FireAlarmRing += new FireAlarmDelegate(employee.RunAway);
            sigma.FireAlarmRing += new FireAlarmDelegate(fireman.RushIntoFire);

            //由你來放火!
            Console.WriteLine("Please input 'FIRE' to fire the building...");
            string str = Console.ReadLine();
            if (str.ToUpper() == "FIRE")
            {
                sigma.OnFire(7, 'C');
            }
        }
    }
}
       我們仔細分析一下上面的代碼:

1.        較之第一個例子,本例中多了一個class FireEventArgs 類,這個類是專門用於傳遞“着火”這件事的參數的——請回過頭去看“現實世界中的事件”一段——我們關心的主要是着火的地點和火的級別,因爲在這個類中我們有兩個成員變量。

2.        接下來的委託也有所改變,由無參變成了兩個參數——object sender, FireEventArgs e ,大家可能會問:爲什麼你寫兩個參數,而不用3個或者4個?唔……傳統習慣就是這樣,這個傳統的開端應該可以追溯到VC的Win32時代吧——那時候,消息傳遞的參數就是一個lParam和一個wParam,分別承載着各種有用的信息——VB模仿它們,一個改名叫sender,一個改名叫e,然後C#又傳承了VB,就有了你看到的樣子。至於爲什麼第一個參數是object型,解釋起來需要用到一些“多態”的知識,在這裏我先不說了,我會在《深入淺出話多態》中以之爲例。第二個參數使用了我們自己製造的類,這個類裏帶着兩個有用的信息,上面也提到過了。

3.        在Building類的OnFire方法函數中,進行了參數的傳遞——你完全可以理解成:Building類將這些參數(一個是this,也就是自己,另一個是e,承載着重要信息)發送給了接收者,也就是發送給了關心/訂閱了這一事件的類。在我們這個例子中,訂閱了Building類事件的對象分別是employee和fireman,他們會在事件發生的時候得到通知,並從傳遞給他們的事件參數中篩選自己關心的內容。這一篩選是由程序員完成的,在本例中,Employee對消息就做了篩選,而Fireman類不加篩選,見火就滅(我有點擔心我左邊的兄弟)。

4.        注意:Building類有一個buildingName域,因爲Building是事件的持有者,所以在事件激發的時候,它也是消息的發送者(sender),所以,這個buildingName域也會隨着this被髮送出去。如果你理解什麼是多態,那麼OK,你會明白爲什麼可以使用Building firePlace = (Building)sender;把這個buildingName讀出來。

5.        Employee類是本程序中最有意思的類。它不但提供了對Build類事件的影響,還會對事件進行智能篩選——只有當自己所工作的大廈警報鳴響,並且是自己所在樓層失火或火勢足夠大時纔會撤離。請仔細分析public void RunAway(objectsender, FireEventArgs e)方法。

6.        與Employee類不同,Fireman類對事件是不加篩選的——你想啦,滅火可是消防員的職責,不論哪裏着火,他們都會勇往直前!

7.        進入主程序,代碼相當清晰——的確是這樣,基本類庫的代碼總是比較複雜(在本例中,基本類庫是指Building、Employee、Fireman這幾個類)。一般情況下,基本類庫與主程序的開發者不是一個人,基本類庫的源代碼一般也不向主程序的開發者開放,而是以DLL(Assembly)文件的形式發放,所以實際開發中整個程序看起來是非常清晰的。

8.                    sigma.FireAlarmRing += new FireAlarmDelegate(employee.RunAway);
            sigma.FireAlarmRing += new FireAlarmDelegate(fireman.RushIntoFire);
通過這兩句你能看出什麼來?呵呵,因爲事件是基於委託的,所以事件也是多播的!如果不明白什麼是多播委託,請參見《深入淺出話委託》。

9.        因爲員工在1F工作,所以7層的C級火不會導致其撤離——只有消防員會響應。

 

 

TO BE CONTINUE


二.事件的由來

       在傳統的面向對象的概念中是沒有“事件”這個概念的。傳統的面向對象概念中只有數據(Data,也稱爲field、域、成員變量)和方法(Method,也就是成員函數、function)。如果我沒記錯,那麼事件這個概念最早出現在微軟的COM技術中,又因爲VB是基於ActiveX(COM的一種)的,所以“事件”這一概念便通過VB廣而推之、爲衆多程序員所熟知並使用的——我就是其中的一員。

       .NET Framework實際上是對COM的更高層級的封裝——要知道,早先.NET Framework這個名字沒有出來之前,它叫“COM3”來着——自然就保留了對事件的支持。

三.事件的意義

       《進化論》說:“物競天擇,合理即存在。”

       微軟說:“我是老大,存在即合理!”

       姑且不管微軟是不是在耍大牌、搞霸權——事件的存在的確給程序的開發帶來了很多方便。從設計層面上講,它使程序在邏輯思維方面變得簡潔清晰、便於維護;從技術層面上講,它把堅澀難懂的Windows消息傳遞機制包裝的漂漂亮亮,極大地降低了程序員入職的門檻兒。

從軟件工程的角度上來看,事件是一種通知機制,是類與類之間保持同步的途徑。

問曰:什麼同步?

答曰:消息同步!

四.事件的本質——對消息傳遞的封裝

事件可以被激發(Fire,也有稱爲“引發”的),一個類所包含的成員事件可以在多種情況下被激發。最典型的:一個按鈕的Click事件,可以由用戶使用鼠標來激發它,也可以由測試這個軟件的另一個軟件通過Win32 API函數來激發它。

我們來簡要討論一下這個Click事件:

其實,如果你瞭解Win32的本質,你應該明白用戶是不可能直接接觸到某個控件的。表面上看,的確是用戶用鼠標點擊了一下按鈕。而實際上,當用戶按下鼠標左鍵的時候是通過鼠標向Windows操作系統發送了一個“左鍵單擊[x,y]點”消息,然後Windows再根據[x,y]的位置把這個消息分配(路由)給應該接收它的控件——這就是Windows的消息傳遞/路由機制。

同理,當你移動鼠標的時候,看似好像指針在隨你的意願移動,而實際上是你的鼠標在以每秒鐘幾百次的頻率把當前位置彙報給Windows操作系統,然後Windows再把一個漂亮的指針“畫”在屏幕上給你看——哈哈,我們都被騙了!

然而這些內容對於C#程序員都是不可見的——都被封裝成了“事件”。因此,從Windows系統的機理上講,事件機制就是對Windows消息傳遞機制的包裝。

       下面的代碼是對Visual Studio 2005自動生成的WinForm程序的一個模擬。讀懂之後,大家可以自己寫一個WinForm,對照剖析其中的機理。

代碼:

//============水之真諦============//
//                                                                  //
//          http://blog.csdn.net/FantasiaX       //
//                                                                 //
//========
上善若水,潤物無聲=========//

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms; //先添加對System.Windows.FormsSystem.Drawing程序集的引用哦!
using System.Drawing;

namespace EmulateWinForm
{
    // 自定義的EmulateForm類,派生自Form類。
    class EmulateForm : Form
    {
        //兩個控件
        private Button myButton;
        private TextBox myTextBox;

        //初始化各個控件和窗體本身,並把控件加入窗體的Controls數組。
        private void InitializeComponent()
        {
            myButton = new Button();
            myTextBox = new TextBox();

            myButton.Location = new System.Drawing.Point(195, 38);
            myButton.Size = new System.Drawing.Size(75, 23);
            myButton.Text = "Click Me";
            myButton.Click += new EventHandler(myButton_Click);//掛接事件處理函數

            myTextBox.Location = new System.Drawing.Point(12, 12);
            myTextBox.Size = new System.Drawing.Size(258, 20);

            Controls.Add(myButton);
            Controls.Add(myTextBox);
            Text = "EmulateForm";
        }

        //myButtonClick事件發生時,EmulateForm類給予的事件響應函數(Event Handler
        void myButton_Click(object sender, EventArgs e)
        {
            myTextBox.Text = "Hello, Event World!";
        }

        //EmulateForm類的構造函數中執行上面的初始化方法
        public EmulateForm()
        {
            InitializeComponent();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            EmulateForm myForm = new EmulateForm();
            Application.Run(myForm);
        }
    }
}
代碼剖析:

1.        要想引用using System.Drawing; using System.Windows.Forms;這兩個Namespace,首先要手動添加對System.Drawing和System.Windows.Forms兩個程序集(Assembly)的引用。

2.        EmulateForm類是自定義的,注意,它派生自Form類。爲了清晰起見,我已經把代碼簡化到了幾乎最簡……只有兩個成員變量。myButton是Button類的一個實例;myTextBox是TextBox類的一個實例。EmulateForm類的成員方法privatevoid InitializeComponent()完全是對真正WinForm程序的模仿,在它的函數體裏,對成員變量進行了實例化、初始化(比如確定大小和位置),並將它們加入了窗體的Controls數組裏。這個函數將在EmulateForm的構造函數裏被執行。

3.        本例中最重要的部分就是對myButton的初始化。注意這句:myButton.Click += newEventHandler(myButton_Click);
myButton.Click是myButton的Click事件,你可能會奇怪:這回怎麼不用自己去聲明一個事件了呢?呵呵,因爲.NET Framework已經爲我們準備好了這個事件,你直接用就好了。不過,我們是爲了探尋底細而來,所以我還得仔細說一說這個事件。

4.        詳細剖析Button.Click事件:首先,事件都是基於委託的,那麼myButton.Click事件是基於哪個委託呢?通過查找MSDN,你可以發現myButton.Click是繼承自Control類,並基於EventHandler這一委託——下面是EventHandler委託的聲明

[SerializableAttribute]
[ComVisibleAttribute(true)]
public delegate void EventHandler (Object sender, EventArgs e)

如果你不太瞭解事件是怎麼聲明的,回過頭去溫習一下《深入淺出話事件(上)》。
在這個聲明中,方括號中的是Attribute,你暫時不用去理會它。關鍵是看EventHandler這個委託:這個委託的參數列表要求它所掛接的函數(對於事件來說就是掛接的事件處理函數)應該具有兩個參數——Object類型的sender和EventArgs類型的e。這兩個參數起什麼作用呢?呵呵,其實非常好玩兒——前面說過了,事件機制是對消息機制的封裝,你可以把消息理解成一枚炮彈,sender就是“誰發射的炮彈”,e就是“炮彈裏裝的什麼東西”,炮彈的目標當然就是消息的接收處理者了。我們仔細回顧一下上篇親手寫的那個FireEventArgs類:這個類裏不是有兩個成員變量嗎?一個是代表着火樓層的floor,一個是代表火級的fireLevel,隨着Building類實例的FireAlarmRing事件引發,FireEventArgs類的實例e就被髮射到了Employee類和Fireman類的實例那裏,這兩個實例再打開“炮彈”根據發射過來的內容給出相應處理。就像真實的戰爭中的炮彈有常規彈、穿甲彈、燃燒彈等等一樣,我們的“消息炮彈”也不只一種,信手拈來幾個與大家共賞一下:
 EventArgs類:這個就是Click事件中使用的那個。算是常規彈吧。因爲用戶點擊按鈕是個非常簡單的事件,不需要它攜帶更多的信息了。
 MouseEventArgs類:是由MouseMove、MouseUp、MouseDown事件發射出來。它的實例攜帶了很多其它的信息,其中最常用的就是一個X和一個Y——用腿肚子想也能想明白,那是鼠標當前的位置。後面的例子中我們給出演示。
 PaintEventArg類:由Paint事件發送出來。這顆炮彈可不簡單,那些非常漂亮的自定義控件都離不開它!在它的肚子裏攜帶有一個Graphics,代表的是你可以在上面繪畫的一塊“畫布”……
OK,先列舉3個吧MSDN裏有它們的全家福,位置是System.EventArgs的Derived Classes樹。微軟在.NET Framework方面可謂下足了功夫,從這些Event Args(事件參數),到各種委託,再到五花八門的事件,都已經爲我們做了良好的封裝,我們只需要拿出來用就是了。

5.        void myButton_Click(object sender, EventArgs e)是EmulateForm類對myButton.Click事件的響應函數(也稱事件處理器,Eventhandler)。注意它的參數列表,是不是與EventHandler委託一致啊:p

6.        主程序沒什麼好說的了——new一個EmulateForm的實例出來,用Application.Run方法執行程序就好了。

7.        順便在這裏做一個糾偏:上面已經解釋過sender是什麼了——它是消息的發送者。我屢次在一些書中發現諸如“事件發送者”這類的話,這是不對的!你想啊,事件只能引發、激發、發生,怎麼可能“發送”呢?不合邏輯……

作業1

      

       建立一個WinForm程序,如圖。包含1個Panel,3個TextBox,1個Button。

要求:

1.    當鼠標在Panel裏滑動時,textBox1和textBox2分別顯示鼠標當前的X和Y。

2.    當鼠標點擊按鈕時,textBox3要顯示Hello Events World!字樣。

提示:

1.    留心MouseMove事件的e

2.    留心Visual Studio 2005使用的是C# 2.0,並且使用partial關鍵字將Form1類的代碼分別存儲在了Form1.cs和Form1.Designer.cs兩個文件裏。

作業2

       將《深入淺出話事件(上)》中嘎子炸鬼子的程序升級至使用事件的版本。(代碼我將在以後的日子裏給出)。

 

OVER

轉自http://blog.csdn.net/fantasiax/article/details/812758
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章