C# 事件

一、前言:前面的随笔中说完了委托,现在看看事件到底可以干什么,在前面的随笔中,使用委托的过程中,有一个很别扭,也很显然易见的问题,就是委托第一次必须初始化用"=",绑定二次事件用"+="这个是非常的,怎么说呢?就是别扭;幸运的是事件就是来解决这个问题(不用初始化,直接使用"+=")的;当然将委托封装成SayHelloManager类中的实例也可以解决这个问题,具体做法参照前面的随笔;

二、概述

1、事件:事件从代码角度看,就是声明了一个委托类型的变量;具体实现代码如下:

using System;
namespace Event{
    public delegate void myEventHandler(string _name);
    class Event1{
       static void Main(string[] args){
            BulidSoftManager bsm=new BulidSoftManager();
            bsm.myevent = BulidByC;   //编译报错  错误详情:Event.BulidSoftManager.myevent只能出现在+=或者-=的左边(在Event.BulidSoftManager中使用除外)
            bsm.myevent+=BulidByCSharp;
            bsm.BulidSoftWalk("OA", bsm.myevent);//上同
       }
       static void BulidByC(string _softName){
             Console.WriteLine(_softName+"  这款软件通过C来编写");
       }
       static void BulidByCSharp(string _softName){
              Console.WriteLine(_softName+"  这款软件通过C#来编写");
       }
    }
    public class BulidSoftManager{
         public event myEventHandler myevent;
         public void BulidSoftWalk(string _softName,myEventHandler bulid){
                bulid(_softName);
         }
    }
}

分析:更具上面的错误提示,这个时候就要借助反编译工具reflactor来看看myevent事件内部的构造;

上面是myevent的基本结构,由两个无返回值的方法(add_myevent和remove_myevent)和一个myevent属性组成。然后点击myevent属性发现下图

恍然大悟,其实myevent事件被编译成了myEventHandler委托的私有委托变量,所以不管你给事件加什么修饰符,最后他都会被编译成目标委托的私有委托变量;

下面是其余两个方法的结构图,贴出来看下:

 好了,根据上面的图解和推断,大致就知道事件的内部大概的运行机制

<1>myevent确实是myEventHandler类型的委托,只不过不管给myevent添加什么修饰符,他都是私有的,因为它会被编译器强制编译成private,

 而add_myevent()和remove_myevent则对应"+="和"-="操作,这两个方法分别用于注册委托类型的方法和取消注册,而这两个方法的访问限制取决于你定义的事件是否对外暴露。

如果你定义的事件是private,那么在外部类中就无法调用这个事件当然也就无法吊用这两个方法;

<2>add_myevent()方法概述

从上图可以看出,在add_myevent()方法内部,实际上调用了System.Delegate的Combine()静态方法,这个方法用于将当前的变量添加到委托链表中。我们前面提到过两次,说委托实际上是一个类;

2、综上所述:得出这么几个结论

<1>事件在定义之后,会被编译器编译成委托类型的变量,而这个变量是定义(封装)该事件的类所私有的,当外部类使用该事件时无法进行赋值操作(也就是"="),但是在定义该事件的类中可以使用;

<2>在事件允许访问的情况下,可以对事件进行"+="和"-="操作,原因上文已说明;

三、实例

现在需要设计一个汽车燃油监测系统,当油量小于10升时:

1、汽车警报器报警  -滴滴声

2、仪表盘显示相应的警告信息

using System;

namespace Event
{
    class Program
    {
        static void Main(string[] args)
        {
            Car car = new Car();
            car.drive();
        }
    }
    internal class Car
    {
        private int _oilmass;
        public int OilMass
        {
            set { _oilmass = value; }
            get { return _oilmass; }
        }
        public  void drive()
        {
            for (int OilMass = 10000000; OilMass >= 0; OilMass--)
            {
                if (OilMass < 10)
                {
                    MakeAlark();
                    ShowWarnMsg();
                    break;
                }
            }
        }
        private void MakeAlark()
        {
            Console.WriteLine("滴滴滴~ 油量不足");
        }
        private void ShowWarnMsg()
        {
            Console.WriteLine("油量不足");
        }
    }
}

 上面这段代码显然能很好的完成基本的预警工作,但是从设计角度来说不是很好,因为假设报警器和显示屏来自不同的生产厂商,那么这样的代码就显得耦合度太高了(也就是说让报警器就干报警你的工作,显示器就干显示的工作),下面就来解耦上面的代码。

解耦上面代码需要用到一种设计模式-观察者模式设计模式

简介:Observer设计模式主要包括以下两种对象:

(1)被监视对象:Subject,它往往包含其他对象感兴趣的东西,上面这个例子中油箱中的油就是Subject(被监视对象);

(2)监视对象:Observer,它监视这Subject,当Subject中的某件事发生后,会告知Observer,Obersver会采取相应的行动。上面例子中显示器和报警器就是监视对象,当油箱中油量小于10升时,报警器和显示器就会做出相应的警报;

上面的例子用观察者模式重写的逻辑大致是这样的:

(1)、显示器和报警器告诉油箱,它们对油量比较感兴趣,那么显示器和油箱就在对应的方法中注册油量这个参数

(2)、油箱则保留对显示器报警器中对应方法的引用(通过事件,因为事件可以以委托链的形式来表达,如果单纯的用委托,那么方法只会覆盖)

代码如下:

using System;

namespace  Event
{
    class Obersver
   {
        static void Main(string[] args)
       {
           Oil oil=new Oil();
           Alarm alarm=new Alarm();
           oil.Warn+=alarm.MakeAlarm;
           oil.Warn+=(new Alarm()).MakeAlarm;
           oil.Warn += Display.ShowMsg;
           oil.burn();
        }
   }
   //第一步:构造被监视对象   -油箱
   internal class Oil
   {
       //第二步:定义自身特有的属性
       private int _oilmass;
      
       //第五步:保留对监视对象中方法的使用到自身特有属性方法的引用
       internal delegate void WarnEventHandler(int _oilmass);
       internal event WarnEventHandler Warn;
       
       internal void burn(){
            for(int i=1000;i>=0;i--){
                _oilmass=i;
                if(_oilmass<3){
                   if(Warn!=null){
                      Warn(_oilmass);
                   }
                }
            }
       }
       
   }  
   //第三步:构造监视对象一
   internal class Alarm
   {
       //第四步:注册感兴趣的参数到自定义的方法中
       internal void MakeAlarm(int _oilmass){
           Console.WriteLine("嘀嘀嘀~,油量达到了{0}L", _oilmass);
       }
   } 
   //第三步:构造监视对象二
   internal class Display
   {  
       //第四步:注册感兴趣的参数到自定义的方法中
       internal static  void ShowMsg(int _oilmass)
       {
           Console.WriteLine("油量达到了{0}L", _oilmass);
       }
   } 
}

ok,上面的代码虽然很好的完成了我们提出的需求,但是还是有一点小问题;

(1)、在事件和委托的命名上没有和.NET FrameWork中的保持一致,在.NET FrameWork中委托都应该以EventHandler结尾。事件的命名为委托的名称去掉EventHandler之后的内容。

(2)、上面的代码符合了提出的需求,而且程序的灵活性也大大的提高了,但是如果,我们需要在Observer端(警报器或者显示器)中显示热水器的生产日期、型号、价格等相关属性,日常生活中的这种例子很多,那么现在的代码结构就无法胜任这一需求,因为如果一个两个属性还好,可以通过字段初始化进去,但是如果字段和属性很庞大的话,那么就需要考虑将热水器的引用传递给Observer端(警报器或者显示器)的方式了。解决方案正是.NET Framework经常使用的事件模型。

3、使用.NET Framework的事件模型来解决上面的问题

在解决上面的问题之前,先了解.Net Framework中的委托和事件的编程规范

(1)、委托类型的名称应该以EventHandler结束

(2)、委托原型的定义:有一个void返回值,并接受两个输入参数,一个是Object类型,一个是EventArgs类型(或者继承EventArgs类)

(3)、事件的命名规范:为委托EventHandler之后剩余的部分

(4)、继承自EventArgs的类型应该以EventArgs结尾

(5)、EventArgs对象包含了Observer端(警报器或者显示器)所感兴趣的数据,在本例是temperature

(6)、Object对象是传递给Observer端(警报器或者显示器)的Subject对象,本例中是热水器对象

ok,介绍完规范之后,开始重构上面的代码,解决上面提出的问题

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

namespace Event
{
    class Program
    {
        static void Main(string[] args)
        {
            Calorifier cr = new Calorifier();
            cr.BoilEvent += new Alarm().MakeAlarm;
            cr.BoilEvent += new Display().ShowMessage;
            cr.BoilWater();
        }
    }
    /// <summary>
    /// 热水器类,观察者模式
    /// </summary>
    public class Calorifier
    {
        public string type = "Bamboo 001";
        public string yieldly = "Tong li";
        public delegate void BoilHandler(Object sender, BoiledEventArgs e);
        public event BoilHandler BoilEvent;
        /// <summary>
        /// 定义BoiledEventArgs类,传递给Observer端(警报器或者显示器)所感兴趣的信息 
        /// </summary>
        public class BoiledEventArgs : EventArgs
        {
            public readonly int Temperature;
            public BoiledEventArgs(int temperature)
            {
                this.Temperature = temperature;
            }
        }
        public void BoilWater()
        {
            for (int i = 0; i < 100; i++)
            {
                if (i >= 95)
                {
                    BoiledEventArgs args = new BoiledEventArgs(i);
                    OnBoiled(args);
                }
            }
        }
        protected void OnBoiled(BoiledEventArgs e)
        {
            BoilEvent(this, e);
        }
    }

    /// <summary>
    /// 报警器
    /// </summary>
    public class Alarm
    {
        public void MakeAlarm(Object sender,Calorifier.BoiledEventArgs e) 
        {
            Calorifier cr = (Calorifier)sender;//拆箱
            Console.WriteLine("Alarm:嘀嘀嘀,水已经 {0} 度了,当前热水器的型号是{1},生产地是{2}", e.Temperature, cr.type,cr.yieldly);
            Console.ReadKey();
        }
    }

    /// <summary>
    /// 显示器
    /// </summary>
    public class Display
    {
        public void ShowMessage(Object sender, Calorifier.BoiledEventArgs e)
        {
            Calorifier cr = (Calorifier)sender;//拆箱
            Console.WriteLine("Display:水已烧开,当前温度:{0}度,当前热水器的型号是{1},生产地是{2}", e.Temperature, cr.type,cr.yieldly);
            Console.ReadKey();
        }
    }
}

ok,完美解决问题,通过拆箱和BoiledEventArgs传递给Observer端(警报器或者显示器)他们所需要的信息。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏wannshan(javaer,RPC)

dubbo 缓存的使用和实现解析

dubbo缓存主要实现,对方法调用结果的缓存。 在服务消费方和提供方都可以配置使用缓存。 以消费方为例,可以配置全局缓存策略,这样所有服务引用都启动缓存 ...

3866
来自专栏生信小驿站

数据处理第3部分:选择行的基本和高级的方法

原文地址:https://suzan.rbind.io/2018/02/dplyr-tutorial-3/ 作者:Suzan Baert 这是系列dplyr...

981
来自专栏FreeBuf

浅析ReDoS的原理与实践

*本文原创作者:MyKings,本文属FreeBuf原创奖励计划,未经许可禁止转载 ReDoS(Regular expression Denial of Ser...

8115
来自专栏草根专栏

.NET Core装饰模式和.NET Core的Stream

Beverage是所有咖啡饮料的抽象类, 里面的cost方法是抽象的. description变量在每个子类里面都需要设置(表示对咖啡的描述).

42513
来自专栏草根专栏

使用 C#/.NET Core 实现单体设计模式

本文的概念内容来自深入浅出设计模式一书 由于我在给公司做内培, 所以最近天天写设计模式的文章.... 单体模式 Singleton 单体模式的目标就是只创建一个...

3395
来自专栏Java开发

Spring 整合 Redis

这里配置就完成了。可以直接在service方法上面开启注解: 有4个注解@Cacheable,@CachePut , @CacheEvict,@CacheCo...

2022
来自专栏恰童鞋骚年

.NET基础拾遗(4)委托、事件、反射与特性

  委托这个概念对C++程序员来说并不陌生,因为它和C++中的函数指针非常类似,很多码农也喜欢称委托为安全的函数指针。无论这一说法是否正确,委托的的确确实现了和...

922
来自专栏我杨某人的青春满是悔恨

走进 RxSwift 之观察者模式

RxSwift 是 ReactiveX 系列的 Swift 版本,如果你之前用过 ReactiveCocoa(RAC) 的话,想必对 Functional Re...

2092
来自专栏TechBox

一份走心的iOS开发规范前言约定(一)命名规范(二)编码规范2.14 内存管理规范本文参考文章其他有价值的文章

6278
来自专栏大内老A

[ASP.NET MVC]通过对HtmlHelper扩展简化“列表控件”的绑定

在众多表单元素中,有一类<select>元素用于绑定一组预定义列表。传统的ASP.NET Web Form中,它对应着一组重要的控件类型,即ListContro...

2015

扫码关注云+社区

领取腾讯云代金券