C#学习笔记(十七)事件(四)事件个人总结

若要写一个事件:首先要明确:一个完整的事件模型五部分组成:事件拥有者、事件、事件响应者、事件处理器、订阅。

用前面的例子:顾客Customer去餐馆点餐Order然后Waiter来服务这个事件。

第一步:事件拥有者和事件响应者

用C#来写,从简单的入手。首先写自己有把握的,比如。先定义两个对象类:一个Customer类,一个Waiter类。其他信息我们啥也不知道。但是我们知道肯定要有这两个人来做这事。

public class Customer
{

}

public class Waiter
{

}

并且去主函数中实例化对象备用:

static void Main(string[] args)
{
    //实例化对象:事件拥有者、事件响应者
    Customer customer = new Customer();
    Waiter waiter = new Waiter();

}

这都是C#最基础的,直接写。成不成,草稿要划一划嘛。 

 第二步:事件

其次我们要明确一点,点餐这个Order事件拥有者是谁?谁点餐,顾客点餐,所以事件拥有者就是顾客。事件响应者就是Waiter。

那么我们就可以在Customer里面就可以写上一个事件:Order。

不论是以字段声明的写法还是在class平级中以委托类型的自定义声明都可以。字段式写法简单,一会看。如果是委托类型的自定义事件声明,就需要在Customer类内声明一个事件类型的字段。并且为事件写上事件处理器的添加器和移除器。注意命名方式:后缀是EventHandler和EventArgs,其中EventArgs基类继承。是最复杂的。在上一篇文章中,已经有了完整过程。非必要不写完整声明。这里咱们只是简单的看看哈。

咱们还是从简单的开始:字段式声明,直接用DotNet给我们准备好的EventHandler方法。

public event EventHandler Order;

按住Ctrl点进去看看定义。

本质还是基于委托。注意其委托方法的参数类型,一个是object,一个EventArgs。要知道这俩参数干啥用的。一会讲。现在不管。(对应下文,VS智能提示补全的事件处理器Action的参数。其实这个委托就是要委托这个事件处理器。所以跟事件处理器的参数类型相同)

我们在Customer类中写上事件:至此五部分就有了三个。

namespace Event07默写一遍
{
    class Program
    {
        static void Main(string[] args)
        {
            //实例化对象:事件拥有者、事件响应者
            Customer customer = new Customer();
            Waiter waiter = new Waiter();

        }
    }

    public class Customer
    {
        // 事件声明好了
        public event EventHandler Order;

    }

    public class Waiter
    {

    }
}

第三步:硬写事件处理器以及订阅

硬写,这个词,就是为了搭起主要架构。在主函数中写事件的订阅。事件的订阅是通过事件订阅操作符+=完成。左边必须是事件,右边是响应者的事件处理器。

写的时候没注意到用的类方法,实例化对象Tab没用上,用类定义的事件处理器,所以导致方法是静态的。最后面有重构。不影响。请继续阅读。 

using System;


namespace Event07默写一遍
{
    class Program
    {
        static void Main(string[] args)
        {
            //实例化对象:事件拥有者、事件响应者
            Customer customer = new Customer();
            Waiter waiter = new Waiter();
            customer.Order += Waiter.Action;
        }
    }

    public class Customer
    {
        public event EventHandler Order;

    }

    public class Waiter
    {
        internal static void Action(object sender, EventArgs e)
        {
            throw new NotImplementedException();
        }
    }
}

通过硬写用VS的智能提示,补全了我们的事件处理器方法体

至此主要的框架搭起来了,主干有了。后面开始补全。 


事件的委托类型定义:  public delegate void EventHandler(object sender, EventArgs e);
事件处理器的定义:          internal static void Action(object sender, EventArgs e)

第一句来源我们在Customer类内声明事件时,查看EventHandler方法时。发现是一个委托类型。

第二句是我们在Waiter类中生成的事件处理器;两者参数相同。提出委托的对象找到了。

所以隐藏在事件处理器背后的是委托。

响应者通过委托把处理器传递给事件。供事件去跨类之间调用。


事件处理器本质就是一个函数方法,当事件发生了,响应者如何做。编程语言表述如何做就是用一个函数方法。

订阅,就是将一个方法作为参数赋值给另一个变量,这样的操作在C#叫做委托。

 何为委托:因为C#是强类型的语言,所有变量或参数必须要有数据类型。如果我们想把function()方法交给别人来完成,比如另起线程执行它。那么这个function()函数,就必须作为一个参数传递给另外一个函数。这个参数必须要有数据类型,C#没办法了,定义了一个这样的数据类型,叫委托:定义一个变量,变量的内容为函数体。换句话说,就是C++中的函数指针。

这个变量保存在栈中,其是函数体在堆中的内存位置。一般说,指向函数地址(位置)。将这个赋值给另外一个变量,作为参数传递。这个参数的类型必须是委托类型才可以接受这个地址值


第四步:完善事件机制并触发。

上面事件模型的五个部分都已经写完了,下面针对每个部分进行完善。

事件有了,如何触发事件。让整个过程动起来。早先的文章中提到:事件是由事件拥有者(事精 or TroubleMaker内部逻辑触发的。。。。别管这么多了,触发这事肯定写在事件拥有者内部。

如何写呢?我们要从我们搭好的框架中来寻找线索。目前Waiter中的事件处理器要求两个参数:我们来看一下这两个参数:

internal static void Action(object sender, EventArgs e)
  • object sender:肯定是事件的发生者,也就说应该传入的是一个对象。这个简单,就是传入实例化对象customer。
  • EventArgs e:想一下,事件的本质功能:通知。那么事件通知的啥?通知的内容就是EventArgs。消息装在了EventArgs中。所以在事件内部需要有消息发送出来。

我们需要补全事件内的消息。这个消息是EventArgs类型。这个消息应该是Order点餐这个事件的消息,有啥消息:比如,点的什么食物、大份小份、几份等。这个消息我们单独抽象出来。

也就是将事件发送的消息单独抽象为一类,于此同时我们也不想造轮子,自己写太多。所以自定义的基础上,派生自EventArgs这个类。

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

有了事件中的消息,此时我们把注意力回归到事件本身。

public event EventHandler Order;

事件此时在类中安安静静的做个事件成员美男子。等着我们去包装然后触发发送。 

触发之前,我们要明确这个事件成员是否为空。不为空才可以发送,并且将消息内容封装到事件消息中。

具体步骤:首先判断事件非空,然后实例化消息,接下来将消息写入事件触发内包装好,等到触发。

public class Customer
{
    public event EventHandler Order;

    public void Action()
    {
        if (this.Order != null)
        {
            // 实例化消息
            OrderEventArgs e = new OrderEventArgs();
            // 将事件消息封装。
            e.DishName = "Beer";
            e.Size = "Large";
            e.Number = 10;
            // 事件触发
            Console.WriteLine("事件将要触发了,请响应者注意!!!");
            Console.ReadLine();
            this.Order.Invoke(this, e);  //会立即调用事件处理器。
            Console.WriteLine("事件触发完毕。");
            Console.ReadLine();
        }
    }
}

我们来看看事件EventHandler方法下事件机制触发的真相。用鼠标选中Invoke,按F1查看MSDN中帮助文档。联机版事件委托的案例

也就是说事件是在事件拥有者内部,通过逻辑Action方法来触发的。其实就是通过Invoke直接调用委托来盘活整个过程

接下来只要在主函数调用Action方法就可以触发事件了。

static void Main(string[] args)
{
    //实例化对象:事件拥有者、事件响应者
    Customer customer = new Customer();
    Waiter waiter = new Waiter();
    customer.Order += Waiter.Action;
    customer.Action();
}

第五步:触发响应。

接下来:我们来重写事件处理器,来观察响应的流程。

public class Waiter
{
    internal static void Action(object sender, EventArgs e)
    {
        Console.WriteLine("我是响应者的事件处理器,我已知道你那点破事发生了。剩下的我懂,交给我吧。") ;
        Console.ReadLine();
    }
}

按两次回车:结果如下。Invoke之后会直接调用事件处理器方法并执行。实现跨类、快速调用绑定的方法。

至此整个事件的过程基本算完成了。但是咱是点餐嘛,业务逻辑代码要完善。

第六步:完善业务逻辑代码

客户已经在事件消息中将餐点好10瓶啤酒,需要服务人员确认下单。并计算金额,客户喝完付费走人。

这里面就新增了好多内容:首先服务人员要将事件消息解读出来。这个解码的过程,必定跟编码(封装)的是同一套东西,否则订阅不了。这个解码编码的规定就是一种约束,这个约束后面讲。现在讲一点好处。

Waiter将EventArgs类型的OrderEventArgs中的消息解读并计算金额,解读肯定是在事件处理器中完成别的地方也解读不了这个信息,这就是一种封装,这种封装就避免了消息数据的泄漏,避免了其他客人点餐信息混串一起。一对一绑定。精准服务并处理。

(代码是对现实世界的模拟和抽象,更是理想化的现实世界。)

public class Waiter
{
    internal static void Action(object sender, EventArgs e)
    {
        Console.WriteLine("我是响应者的事件处理器,我已知道你那点破事发生了。剩下的我懂,交给我吧。") ;
        Console.ReadLine();
        Customer customer = sender as Customer;
        OrderEventArgs orderInfo = e as OrderEventArgs;
        Console.WriteLine("Waiter serve you the dish {0}.",orderInfo.DishName);
        //服务方负责计算金额,并且知晓价格,实际中价格应该公开。此处为了简化。
        double price = 10;
        switch (orderInfo.Size)
        {
            case "Large":
                price = price * 2;
                break;
            case "Small":
                price = price * 0.75;
                break;
            default:
                break;
        }
        customer.Bill += price * orderInfo.Number;
    }
}

 想到哪里写到哪里。然后借助VS硬写。

账单有了,Customer功能也需要完善。Customer需要喝完,然后付账单。

public class Customer
{
    public double Bill { get; internal set; }

    public event EventHandler Order;

    // 实例化消息,为了避免浪费,点多少喝多少,把数据类内共享
    // 实例化放在了方法外。
    OrderEventArgs e = new OrderEventArgs();
    public void Action()
    {
        if (this.Order != null)
        {
            // 将事件消息封装。
            e.DishName = "Beer";
            e.Size = "Large";
            e.Number = 10;
            // 事件触发
            Console.WriteLine("事件将要触发了,请响应者注意!!!");
            Console.ReadLine();
            this.Order.Invoke(this, e);
            this.Drink();
            this.PayTheBill();
            Console.WriteLine("事件触发完毕。");
            Console.ReadLine();
        }
    }
    public void Drink()
    {
        Console.WriteLine("开始整:");
        for (int i = 0; i < e.Number; i++)
        {
            Thread.Sleep(500);
            Console.WriteLine("Drink-{0}.....",i);
        }

    }
    public void PayTheBill()
    {
        Console.WriteLine("各位,今晚全场的消费由赵公子买单!");
        Console.WriteLine("这桌消费了{0}。", this.Bill);
    }
}

 


反思: 

五种颜色:五个部分。

(shei)是的,怎么做就是的了,订阅了说明你关注这事。

代码中不可能多管闲事,现实中可能是吃咸萝卜操淡心。代码中不管就是没有订阅,现实中可能是真不关心,还有可能是就是能力不够管不了,订阅不了。

问题:现实中可能能力不够管不了,代码中为啥能力不够订阅不了?

回答:通过VS智能补全的事件处理器中的参数我们可以知道,我们的事件处理器只能接受指定的事件,或者说指定类型的事件,或者说指定内容的事件。

一个事件可以绑定多个事件处理器。一个事件处理器也可以绑定多个事件。但并非随意一个事件处理器就能绑定任意一个事件。能否响应订阅,要看他们之间的约束是否满足。

因为委托中定义(约束)了的参数的类型。你的事件处理器中的参数类型,跟人家的不一致。这个约束条件是通过委托传给你的。委托嘛,可能所托非人。你就假装委托人没通知你。如果我们可以通过定义多个事件来看。

(我们写代码的思路跟正好相反,是通过事件处理器反推的事件中传递消息的类型和事件。设计的时候,这个过程正面推进。)


结束语: 

真正我们设计的地方:事件可以定义多个。针对每一个事件,设计不同的消息类型类,对不同的事件拥有者来绑定不同的事件处理器


文末附:整个代码:(事件处理器本例中是静态的,其实可以修改为动态的,这样可以不断实例化waiter服务更多人。文后有重构。)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Event07默写一遍
{
    class Program
    {
        static void Main(string[] args)
        {
            //实例化对象:事件拥有者、事件响应者
            Customer customer = new Customer();
            Waiter waiter = new Waiter();
            customer.Order += Waiter.Action;
            customer.Action();
        }
    }

    public class Customer
    {
        public double Bill { get; internal set; }

        public event EventHandler Order;

        // 实例化消息,为了避免浪费,点多少喝多少,把数据类内共享
        // 实例化放在了方法外。
        OrderEventArgs e = new OrderEventArgs();
        public void Action()
        {
            if (this.Order != null)
            {
                // 将事件消息封装。
                e.DishName = "Beer";
                e.Size = "Large";
                e.Number = 10;
                // 事件触发
                Console.WriteLine("事件将要触发了,请响应者注意!!!");
                Console.ReadLine();
                this.Order.Invoke(this, e);
                this.Drink();
                this.PayTheBill();
                Console.WriteLine("事件触发完毕。");
                Console.ReadLine();
            }
        }
        public void Drink()
        {
            Console.WriteLine("开始整:");
            for (int i = 0; i < e.Number; i++)
            {
                Thread.Sleep(500);
                Console.WriteLine("Drink-{0}.....",i);
            }

        }
        public void PayTheBill()
        {
            Console.WriteLine("各位,今晚全场的消费由赵公子买单!");
            Console.WriteLine("这桌消费了{0}。", this.Bill);
        }
    }

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

    public class Waiter
    {
        internal static void Action(object sender, EventArgs e)
        {
            Console.WriteLine("我是响应者的事件处理器,我已知道你那点破事发生了。剩下的我懂,交给我吧。") ;
            Console.ReadLine();
            Customer customer = sender as Customer;
            OrderEventArgs orderInfo = e as OrderEventArgs;
            Console.WriteLine("Waiter serve you the dish {0}.",orderInfo.DishName);
            //服务方负责计算金额,并且知晓价格,实际中价格应该公开。此处为了简化。
            double price = 10;
            switch (orderInfo.Size)
            {
                case "Large":
                    price = price * 2;
                    break;
                case "Small":
                    price = price * 0.75;
                    break;
                default:
                    break;
            }
            customer.Bill += price * orderInfo.Number;
        }
    }
}

代码重构:类内不具体点餐。类外实现。多人点餐,各自结算。各自绑定各自的事件。其实可以把点餐再抽象一步。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Event08重构
{
    class Program
    {
        static void Main(string[] args)
        {
            //实例化对象:事件拥有者、事件响应者
            Customer customer1 = new Customer(1);
            Waiter waiter1 = new Waiter(1);
            customer1.Order += waiter1.Serve;
            customer1.Think("Beer", "Large", 10);

            Customer customer2 = new Customer(2);
            Waiter waiter2 = new Waiter(2);
            customer2.Order += waiter2.Serve;
            customer2.Think("Chicken", "Small", 5);

            Customer badGuy = new Customer(13);
            Waiter waiter11 = new Waiter(11);
            badGuy.Order += waiter11.Serve;
            // 这个坏蛋想吃满汉全席,让1号服务员服务,挂账到1号桌。结果还是自己账单。
            badGuy.Order += waiter1.Serve;
            badGuy.Think("Manhanquanxi", "Large", 10);
            Console.ReadLine();
        }
    }

    public class Customer
    {
        public double Bill { get; internal set; }
        public int DeskID { get; set; }
        public int DishNumber { get; set; }

        public Customer(int deskID)
        {
            this.DeskID = deskID;
        }
        //声明事件
        public event EventHandler Order;

        // 触发器一般命名方式是On+事件。“事出有因”,“因何引发”
        // 这个方法的级别应该是protected,否则又可以被其他人利用,用来借刀杀人。
        // 如果改为protected那么实例无法直接访问,为此。需要新的方法再类内调用工外部使用。
        // 所以才有了说事件是由拥有者内部逻辑触发的。
        protected void OnOrder(OrderEventArgs e)
        {
            // 类似“单例模式”中判断窗口字段是否已存在。
            if (this.Order != null)
            {
                this.Order.Invoke(this, e);
                this.Eat(e.Number);
                this.PayTheBill();
            }
        }

        public void Think(string dishName, string size, int number)
        {
            OrderEventArgs e = new OrderEventArgs();
            e.DishName = dishName;
            e.Size = size;
            e.Number = number;
            this.OnOrder(e);
        }

        public void Eat(int EatNumber)
        {
            Console.WriteLine("开始干饭:");
            for (int i = 0; i < DishNumber; i++)
            {
                Thread.Sleep(100);
                Console.WriteLine("Eat-{0}.....", i);
            }
        }
        public void PayTheBill()
        {
            Console.WriteLine("{0}这桌消费了{1}。", this.DeskID,this.Bill);
        }
    }

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

    public class Waiter
    {
        public int ServeID { get; set; }
        public Waiter(int serveID)
        {
            this.ServeID = serveID;
        }
        public void Serve(object sender, EventArgs e)
        {
            Customer customer = sender as Customer;
            OrderEventArgs orderInfo = e as OrderEventArgs;
            Console.WriteLine("Waiter{0} serve you the dish {1}.", this.ServeID, orderInfo.DishName);
            double price = 10;
            switch (orderInfo.Size)
            {
                case "Large":
                    price = price * 2;
                    break;
                case "Small":
                    price = price * 0.75;
                    break;
                default:
                    break;
            }
            customer.Bill += price * orderInfo.Number;
            customer.DishNumber += orderInfo.Number;
        }
    }
}

如果想让一个类成员只能够被自己的派生类访问,不能够被其他的类访问的时候,这些成员的访问级别就应该是protected。不能为public。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值