什么是事件?


需要了解:

  1. 事件的概念
  2. 事件的應用
  3. 理解事件與委托
  4. 事件的聲明
  5. 問題辨析(事件是特殊的委托嗎?)

事件的概念

  • 定義:Event,譯為“事件”
  • 角色:使對象或類具備通知能力的成員  
    • 中譯:事件是一種使對象或類能夠提供通知的成員。
    • 英譯:An event is a member that enables an object or class to provide notifications.
    • “對象O擁有一個事件E” 想表達的意思是,當事件E發生的時候,O有能力通知其他的對象。
      • 當手機響了的時候,在手機的角度來看,它是在通知關注他的人,要求關注它的人采取行動,而在人的角度來看,人收到了通知,可以采取行動了。更進一步的說
        • 伴隨着通知,很多通知在發生的時候會伴隨着很多信息的出現,以我們的手機為例,如果手機響了,可能是你收到了微信,或者是工作通知,或者是app推送。站在手機的角度來看,它是在通知關注者的同時,將相關的消息也發送給關注者了。而站在關注者的角度來看,他除了被通知到了之外,還收到了事件的主體(手機)經由事件發送過來的消息。而在C#中,這種消息被稱為 “事件參數”(EventArgs),被通知者根據參數里的內容,來采取相應的行動,如果是會議體系就准備參加會議,如果是電話則根據電話號碼是接聽還是不接聽,而我們根據通知和事件參數來采取行動的這種行為成為響應事件or處理事件,處理事情時具體所做的事情被稱為 “事件處理器”(EventHandler)
        • 不是所有的事件,都帶有信息,例如大樓的火警預報響了,在它發起通知后,你沒有進行判斷就已經跑路了,此類型的事件,當發生時就說明了一切。所以事件 = 通知 +(可選)的參數。
  • 使用:用於對象或類之間的動作協調與消息傳遞(消息推送)
    • 現在當別人告訴你這個類或對象中有一個事件,說明這個類在發送通知了之后,關注它的訂閱者們也就會根據事件的類型,事件的信息,紛紛做出動作協調。
  • 原理:事件模型(event model)中的兩個5
    • “發生--響應”中的五個部分 —— 鬧鍾響了你起床,孩子餓了你做飯....這里面還隱含了“訂閱關系”。你只關心你的鬧鍾和你的孩子,這便是訂閱。
    • “發生--響應”中的五個步驟 —— (1)我有一個事件 -- (2)一個人或者一群人關心我的這個事件 -- (3) 我的這個事件發生了 --(4) 關心這個事件的人依次被通知到 --(5)被通知到的人根據拿到我的事件信息(又稱“事件數據”,“事件參數”,“通知”)對事件進行響應(又稱處理事件)。

事件的應用:

  事件模型的五個組成部分:

    1. 事件的擁有者(event source,對象)
    2. 事件的成員 (event,成員)
    3. 事件的響應者 (event subscriber,對象)
    4. 事件的處理器 (event handler,成員)—— 本質是一個回調方法
    5. 事件訂閱 ——把事件處理器與事件關聯到一起,本質上是一種以委托類型為基礎的 “約定”
      • 約定是指在C#中,用於訂閱事件的事件處理器,必須和事件遵守同一個約定,這個約定即表明了事件能夠把什么樣的消息發送給事件處理器,也約束了事件處理器能夠處理什么樣的消息。當遵守了這個約定之后,我們便可以認為事件和事件處理器是匹配的。則可以用事件處理器去訂閱事件了。否則不匹配,不能夠訂閱。

  事件模型的應用組合方式:

  1. 事件的擁有者和事件的響應者分別是兩個不同的對象,且都包含者他們的事件成員

  

using System;
using System.Timers;

namespace LanguageLearn
{
    class Program
    {
        static void Main(string[] args)
        {
            //事件的擁有者
            var timer = new Timer();
            timer.Interval = 1000;

            //事件的響應者
            var boy = new Boy();
            var girl = new Girl();

            //訂閱
            timer.Elapsed += boy.Action;
            timer.Elapsed += girl.Action;
            timer.Start();

            Console.ReadLine();
        }
    }

    class Boy
    {
        //事件的處理器
        internal void Action(object sender, ElapsedEventArgs e)
        {
            Console.WriteLine("Jump!");
        }
    }

    class Girl
    {
        internal void Action(object sender, ElapsedEventArgs e)
        {
            Console.WriteLine("Sing!");
        }
    }
}
View Code

  2. 事件方法訂閱了本身的方法

  

using System;
using System.Windows.Forms;

namespace LanguageLearn4._8
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Form form= new Form();
            Controller controller= new Controller(form);

            form.ShowDialog();
        }
    }

    class Controller
    {
        private Form _form;
        
        public Controller(Form form)
        {
            if (form != null)
            {
                _form = form;
                _form.Click += this.FormClicked;
            }       
        }

        private void FormClicked(object sender, EventArgs e)
        {
            _form.Text = DateTime.Now.ToString();
        }
    }

}
View Code

  3. 事件的響應者是事件的擁有者的一部分,比如一個大菜單欄通知了事件,而事件的響應者是里面的小菜單

  

using System;
using System.Windows.Forms;

namespace LanguageLearn4._8
{
    internal class Program
    {
        static void Main(string[] args)
        {
            //事件的擁有者 也是事件的響應者
            MyForm form= new MyForm();
            //事件的源 以及事件的訂閱
            form.Click += form.FormClick;

            form.ShowDialog();
        }
    }

    class MyForm : Form
    {
        //事件的處理程序
        internal void FormClick(object sender, EventArgs e)
        {
            this.Text = DateTime.Now.ToString();
        }
    }

}
View Code

   4. 事件的擁有者是事件的響應者的一部分,這種模式最常見,如我們的應用程序和按鈕的關系。

  

using System;
using System.Windows.Forms;

namespace LanguageLearn4._8
{
    internal class Program
    {
        static void Main(string[] args)
        {
            MyForm form = new MyForm();

            form.ShowDialog();
        }
    }

    //事件的擁有者為事件的響應者的一部分
    class MyForm : Form
    {
        private TextBox _textBox;
        private Button _button;

        public MyForm()
        {
            this._textBox = new TextBox();
            this._button = new Button();
            this.Controls.Add(this._textBox);
            this.Controls.Add(this._button);
            this._button.Text = "Click";
            //事件
            this._button.Click += this.ButtonClicked;
            this._button.Top = 100;
        }

        //事件處理器
        private void ButtonClicked(object sender, EventArgs e)
        {
            this._textBox.Text = DateTime.Now.ToString();
        }
    }

}
View Code

 

理解事件與委托:

  • 事件是基於委托的:
    1. 事件需要用於委托類型來做一個約束,約束規定了事件能發送什么樣的消息給響應者,也規定了響應者能收到什么樣的消息。
    2. 事件響應的事件處理器必須與約束所匹配。
    3. 當事件的響應者向事件的擁有者提供了能夠匹配這個事件的事件處理器之后呢,需要把事件處理器保存和記錄下來。能夠記錄和引用方法也只有委托類型的實例能夠做到。

    綜上所述,無論是表層的約束,還是底層的實現,都是需要依賴於委托類型的。

  • 有了委托字段/屬性,為什么還需要事件?

    為了讓程序的邏輯更加“有道理”、更加安全,防止“借刀殺人”。有了委托作為條件限制的事件,他在對象外部只允許 += or -= 也即是訂閱事件和取消訂閱事件,而在方法內部才允許發起事件的流程,這為我們提供了一定的保護性,不允許一個對象發起另一個對象的事件,導致程序的混亂,這就是為什么安全的道理。

    小例子:  

using System;
using System.Threading;

namespace LanguageLearn
{
    class Program
    {
        static void Main(string[] args)
        {
            Customer customer = new Customer();
            Waiter waiter = new Waiter();

            //事件訂閱
            customer.Order += waiter.Action;
            
            // 顧客沒有進行點菜
            //customer.Action();

            //不安全的對象 惡意的讓customer進行委托點菜
            Customer badCustomer = new Customer();
            //這時候委托對象是一個字段 可以調用Invoke方法。
            OrderEventArgs orderEventArgs = new OrderEventArgs()
            {
                DishName = "幫你點的菜",
                Size = "big"
            };
            badCustomer.Order += waiter.Action;
            badCustomer.Order.Invoke(customer, orderEventArgs);

            //用戶需要給錢  離譜哇
            customer.PayBill();
        }
    }

    //用於傳遞事件消息
    public class OrderEventArgs : EventArgs
    {
        public string DishName { get; set; }
        public string Size { get; set; }
    }

    //用來處理Event 使用EventHandler作為后綴
    //委托用來約束事件處理器 在此處消息約束對象是顧客 帶有的信息為訂單消息
    public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);

    //事件的擁有者
    public class Customer
    {
        //錯誤的事件聲明 聲明成了委托
        public OrderEventHandler Order;

        public double Bill { get; set; }
        public void PayBill()
        {
            Console.WriteLine($"I will pay ${Bill}");
        }

        public void WalkIn()
        {
            Console.WriteLine("Walk into the restaurant");
        }

        public void SitDown()
        {
            Console.WriteLine("Sit on a Desk");
        }

        public void Think()
        {
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine("Let me think... ");
                Thread.Sleep(1000);
            }

            //若沒有事件處理器來響應我們的事件 則不應該觸發事件
            if (this.Order != null)
            {
                //發起事件
                this.Order.Invoke(this, 
                    new OrderEventArgs { DishName = "Kongpao Chicken", Size = "big" });
            }
        }

        public void Action()
        {
            Console.ReadLine();
            this.WalkIn();
            this.SitDown();
            this.Think();
        }
    }

    //事件的響應者
    public class Waiter
    {
        public void Action(Customer customer, OrderEventArgs e)
        {
            Console.WriteLine($"I will Server the Dish - {e.DishName}");
            double price = 10;
            switch (e.Size)
            {
                case "small":
                    price *= 0.5;
                    break;
                case "big": 
                    price *= 1.5;
                    break;
                default:
                    break;
            }

            customer.Bill += price;
        }
    }
}
View Code

    輸出:

    

  • 所以事件的本質是委托字段的一個包裝器
    • 這個包裝器對委托字段的訪問起限制作用,是一層“蒙板”。
    • 在OO中封裝的一個重要功能,就是隱藏
    • 事件對外界隱藏了委托實例的大部分功能,僅暴露添加/移除事件處理器的功能。
  • 事件與委托的關系
    • 事件真的是 “以特殊方式聲明的委托字段/實例” 嗎?
      • 不是!只是聲明的時候看起來像(對比委托字段與事件的簡化聲明,filed-like)。但是你反編譯之后還是可以看見事件的完整生命。
      • 事件聲明的時候使用了委托類型,簡化聲明造成事件看上去像一個委托字段,而event關鍵字更像是一個修飾符。——這是錯覺來源之一。
      • 訂閱事件的時候 += 操作符后面可以是一個委托實例,這與委托實例的賦值方法相同,這也讓事件看起來更像是一個委托字段。——這是錯覺來源之一。
      • 重申:事件的本質是加裝在委托字段上的一個“蒙板”(Mask),是起一個掩蔽作用的包裝器,這個用於抵御非法操作的“蒙板”絕不是委托字段本身。
  • 為什么要使用委托類型來聲明事件?
    • 站在sourse的角度講,是為了表明source能傳遞出哪些信息。
    • 站在subscriber的角度講,它是一種約定,它約定了你應該用什么樣的簽名的方法來處理(響應)事件。
    • 委托類型的實例將用於存儲(引用)事件處理器。
  • 類比事件和屬性
    • 屬性不是字段——很多時候屬性是字段的包裝器,這個包裝器保證字段不被亂用。有時候是字段的邏輯判斷器,保證字段是一個合法的值。
    • 事件不是委托字段——他是委托字段的包裝器,這個包裝器用來保護委托字段不被濫用
    • 包裝器永遠都不可能是被包裝的東西。

事件的聲明:

  • 完整聲明:

  下面一個例子是 模擬顧客進飯店點菜吃完結賬的一個事件流程。

using System;
using System.Threading;

namespace LanguageLearn
{
    class Program
    {
        static void Main(string[] args)
        {
            Customer customer = new Customer();
            Waiter waiter = new Waiter();

            //事件訂閱
            customer.Order += waiter.Action;

            customer.Action();
            customer.PayBill();
        }
    }

    //用於傳遞事件消息
    public class OrderEventArgs : EventArgs
    {
        public string DishName { get; set; }
        public string Size { get; set; }
    }

    //用來處理Event 使用EventHandler作為后綴
    //委托用來約束事件處理器 在此處消息約束對象是顧客 帶有的信息為訂單消息
    public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);

    //事件的擁有者
    public class Customer
    {
        //事件的完整性委托約束
        private OrderEventHandler orderEventHandler;
        public event OrderEventHandler Order
        { 
            add { this.orderEventHandler += value; }
            remove { this.orderEventHandler -= value; }
        }

        public double Bill { get; set; }
        public void PayBill()
        {
            Console.WriteLine($"I will pay ${Bill}");
        }

        public void WalkIn()
        {
            Console.WriteLine("Walk into the restaurant");
        }

        public void SitDown()
        {
            Console.WriteLine("Sit on a Desk");
        }

        public void Think()
        {
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine("Let me think... ");
                Thread.Sleep(1000);
            }

            //若沒有事件處理器來響應我們的事件 則不應該觸發事件
            if (this.orderEventHandler != null)
            {
                //發起事件
                this.orderEventHandler.Invoke(this, 
                    new OrderEventArgs { DishName = "Kongpao Chicken", Size = "big" });
            }
        }

        public void Action()
        {
            Console.ReadLine();
            this.WalkIn();
            this.SitDown();
            this.Think();
        }
    }

    //事件的響應者
    public class Waiter
    {
        public void Action(Customer customer, OrderEventArgs e)
        {
            Console.WriteLine($"I will Server the Dish - {e.DishName}");
            double price = 10;
            switch (e.Size)
            {
                case "small":
                    price *= 0.5;
                    break;
                case "big": 
                    price *= 1.5;
                    break;
                default:
                    break;
            }

            customer.Bill += price;
        }
    }
}
事件完整式聲明
  • 簡略聲明(字段式的聲明 field-like):
using System;
using System.Threading;

namespace LanguageLearn
{
    class Program
    {
        static void Main(string[] args)
        {
            Customer customer = new Customer();
            Waiter waiter = new Waiter();

            //事件訂閱
            customer.Order += waiter.Action;

            customer.Action();
            customer.PayBill();
        }
    }

    //用於傳遞事件消息
    public class OrderEventArgs : EventArgs
    {
        public string DishName { get; set; }
        public string Size { get; set; }
    }

    //用來處理Event 使用EventHandler作為后綴
    //委托用來約束事件處理器 在此處消息約束對象是顧客 帶有的信息為訂單消息
    public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);

    //事件的擁有者
    public class Customer
    {
        //事件的簡略式聲明
        public event OrderEventHandler Order;

        public double Bill { get; set; }
        public void PayBill()
        {
            Console.WriteLine($"I will pay ${Bill}");
        }

        public void WalkIn()
        {
            Console.WriteLine("Walk into the restaurant");
        }

        public void SitDown()
        {
            Console.WriteLine("Sit on a Desk");
        }

        public void Think()
        {
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine("Let me think... ");
                Thread.Sleep(1000);
            }

            //若沒有事件處理器來響應我們的事件 則不應該觸發事件
            if (this.Order != null)
            {
                //發起事件
                this.Order.Invoke(this, 
                    new OrderEventArgs { DishName = "Kongpao Chicken", Size = "big" });
            }
        }

        public void Action()
        {
            Console.ReadLine();
            this.WalkIn();
            this.SitDown();
            this.Think();
        }
    }

    //事件的響應者
    public class Waiter
    {
        public void Action(Customer customer, OrderEventArgs e)
        {
            Console.WriteLine($"I will Server the Dish - {e.DishName}");
            double price = 10;
            switch (e.Size)
            {
                case "small":
                    price *= 0.5;
                    break;
                case "big": 
                    price *= 1.5;
                    break;
                default:
                    break;
            }

            customer.Bill += price;
        }
    }
}
簡略式聲明
  • 用於聲明事件的委托類型的命名約定  
    • 用於聲明Foo的事件的委托,一般命名為FooEventHandler。
    • FooEventHandler委托的參數一般有兩個
      • 第一個是object類型的,名字為sender,實際上就是事件的擁有者,事件的source。
      • 第二個是EventArgs類的派生類,類名一般為FooEventArgs,參數名為e,也就是事件參數,事件發生時帶來的信息。
      • 我們可以把委托的參數列表看作是事件發生后發送給事件響應者的“事件消息”。
    • 觸發Foo事件的方法一般命名為OnFoo,即“事出有因”
      • 訪問級別為protected,不能為public,不然又可以“借刀殺人”
  • 事件的命名約定
    • 帶有時態的動詞或動詞短語
    • 事件擁有者“正在做”什么事情,使用進行時;事件擁有者“做完了什么事情”,用完成時。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM