[C#] 事件(Event)使用心得筆記

事件(Event)使用心得筆記,包含自訂事件使用範例

太久沒更新了,不趕快把一些還記得的記下來就怕又真地失憶去了!

先把事件使用上互動的幾個腳色先拿出來說嘴一下

使用事件時,都要注意這五個腳色是否都存在或是由何處做相關操作

  • 事件擁有者/事件發送者 (Event Owner/Source):擁有事件的類別或物件,而該類別/物件本身的作業流程/邏輯會觸發事件發生
  • 事件 (Event):事件本體,透過委派(delegate)來實作
  • 事件訂閱者/事件響應者 (Event Subscriber):擁有反應事件發生時事件方法之類別/物件
  • 事件處理器 (Event Handler):當事件發生時,對應要處理的事件方法;約定俗成使用 -EventHandler 做結尾
  • 訂閱 (Subscribe):連結事件與要做出對應處理的方法的機制

上面快速理解是『事件擁有者』與『事件』是一組,『事件訂閱者/事件響應者』與『事件處理器』是一組,並透過『訂閱』機制使兩組物件做連結

這裡透過Youtube上Tim老師的顧客與點餐的例子來理解,非常推薦該C#教學系列的事件講解!事件詳解(上)事件詳解(中)事件詳解(下) (非常需要專注力跟細心!!該系列真的很棒的!)

這個例子是兩個類別 - 顧客與服務生,顧客進餐廳點餐,服務生響應訂餐這個事件,最終再依據服務生類別內算出的金額,顧客做出結帳動作(這邊顧客跟服務生內的 Action 方法是不一樣的唷!)

首先先宣告出點餐的事件參數,用來處理想點的餐點名稱與餐點分量;這部分不能放在事件擁有者類別內喔!不然無法跨類別使用!

public class OrderEventArgs : EventArgs
{
    public string DishName { get; set; }
    public string Size { get; set; }
}

再來我們來宣告一下客戶要做的事與點餐事件

// 宣告一個事件委託
public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);

// 事件擁有者/Source
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 PayTheBill()
    {
        Console.WriteLine("Customer: I will pay ${0}.", this.Bill);
    }

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

    public void SitDown()
    {
        Console.WriteLine("Customer: Sit down.");
    }

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

        // 此處觸發事件發生
        if (this.orderEventHandler != null)
        {
            OrderEventArgs e = new OrderEventArgs();
            // 訂個大份宮保雞丁
            e.DishName = "Kongpao Chicken.";
            e.Size = "large";
            this.orderEventHandler.Invoke(this, e);
        }
    }

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

然後再加上服務生類別,主要功能是等待顧客訂餐然後做出反應與算出金額

// 事件響應者/Subscriber
public class Waiter
{
    // 事件處理器
    public void Action(Object Sender, OrderEventArgs e)
    {
        Customer customer = Sender as Customer;

        Console.WriteLine("Waiter: I will serve you the dish - {0}", e.DishName);
        double price = 10;

        switch (e.Size)
        {
            case "small":
                price = price * 0.5;
                break;
            case "large":
                price = price * 1.5;
                break;
            default:
                break;
        }

        customer.Bill += price;
    }
}

完成以上的宣告後,主程式內容就如下

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

        // waiter 的 Action 事件處理器訂閱 customer 的 Order 事件
        customer.Order += waiter.Action;
        customer.Action();
        customer.PayTheBill();
    }
}

以上例子是非常正規完整的自訂事件的流程!


看到這個水平線就是開始魔改進化的部分了!XD

解說就放到程式註解內

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

        // waiter 的 Action 事件處理器訂閱 customer 的 Order 事件
        customer.Order += waiter.Action;
        customer.Action();
        customer.PayTheBill();
    }
}

// 事件參數
public class OrderEventArgs : EventArgs
{
    public string DishName { get; set; }
    public string Size { get; set; }
}

// 事件擁有者/Source
public class Customer
{
    // 簡化語法事件宣告,可以省略一個 delegate 的宣告與事件包裝器
    public event EventHandler Order;

    public double Bill { get; set; }
    public void PayTheBill()
    {
        Console.WriteLine("Customer: I will pay ${0}.", this.Bill);
    }

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

    public void SitDown()
    {
        Console.WriteLine("Customer: Sit down.");
    }

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

        this.OnOrder("Kongpao Chicken.", "large");
    }

    // 獨立出來符合 OOP 的 Single Responsibility Principle
    // 一般約定俗成使用 On- 開頭宣告方法表示觸發何種事件;且基本上使用 protected 修飾詞,使用 public 會有優先級別太廣泛的問題
    protected void OnOrder(string dishName, string size)
    {
        if (this.Order != null)
        {
            OrderEventArgs e = new OrderEventArgs();
            e.DishName = dishName;
            e.Size = size;

            this.Order.Invoke(this, e);
        }
    }

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

// 事件響應者/Subscriber
public class Waiter
{
    // 事件處理器,使用通用宣告變數,前提條件式原本事件消息(參數)類別必須衍生自 EventArgs 類別,且在此將對應參數做轉型
    public void Action(Object Sender, EventArgs e)
    {
        Customer customer = Sender as Customer;
        OrderEventArgs orderInfo = e as OrderEventArgs;

        Console.WriteLine("Waiter: I will serve you the dish - {0}", orderInfo.DishName);
        double price = 10;

        switch (orderInfo.Size)
        {
            case "small":
                price = price * 0.5;
                break;
            case "large":
                price = price * 1.5;
                break;
            default:
                break;
        }

        customer.Bill += price;
    }
}

第一個例子是使用事件包裝器(add/remove)來宣告一個事件,如使用(get/set)宣告一個屬性(Property)一樣;而第二個例子直接使用簡略語法宣告,可以省略一個委派宣告

但是不變的是都要用 event 關鍵字修飾,這個關鍵字主要作用在限制原本委派可能造成非原本事件擁有者的其他類別物件去觸發調用該事件!

一旦加上 event 之後,該事件只能使用在 +=/-= 的左邊(添加事件處理器/移除事件處理器)!只能由該事件擁有者觸發該事件!

且要注意的是每次要觸發事件時,必須判斷一下封裝該事件的委派是否為空!

所以如果向以下的事件觸發寫法

if (Order != null)
{
    Order.Invoke(this, e);
}

也利用[C#] 「?」符號使用心得筆記的用法改寫為

Order?.Invoke(this, e);

其他更簡易用法,網路上可搜尋到更多,這裡先標註自己容易遺忘的部分了~再度完結灑花~XD