C#中的委托和事件

定义:委托是一种引用类型,表示对具有特定参数列表和返回类型的方法的引用。 在实例化委托时,你可以将其实例与任何具有兼容签名和返回类型的方法相关联
目的:方法声明和方法实现的分离,使得程序更容易扩展

一、对委托的理解

1. 为什么将方法作为另一个方法的参数

先不解释定义,看一段代码

  public void Method1(object obj)
   {
      //内部可以访问obj的成员
   }

这是随便写的一个方法,没有实际意义,但是,根据我们已掌握的关于类型的基础知识,应该明白,这里的obj(引用类型)作为形参,存放的是对象的引用,既然获取到了对象的引用,那么我们可以在run方法内部对obj的成员进行访问(一段废话)。好了,现在我要问一个问题:为什么要将obj作为参数?

问题先慢慢想着,我们再次看一下委托的定义,"是引用类型,对方法的引用",看下面的代码

  public void Method2(delegate del)
   {
      //内部可以访问del的什么?
      //只能执行方法
      del();
   }

delegate 作为一种引用类型,引用的是个方法,我们能对方法做什么,只能执行方法。
下面我们回答刚才的问题,obj作为参数(类型声明),将类型的声明和类型的实例分离。当然这样做的目的是为了封装变化,所有类型都可以作为实参来使用Method1,因为Object是基类,当然也可以将Object换成其他接口类型,只要实现了该接口的类型都可以作为Method1的实参。
虽然Method1和Method2参数类型不一样,但是目的是一致的,委托是将方法的声明和实现分离。
下面看一个网上使用广泛的例子

    示例1
     //定义委托,与任何具有兼容签名和返回类型的方法相关联
     public delegate void GreetingDelegate(string name);
    class Program
    {
        private static void EnglishGreeting(string name)
        {
            Console.WriteLine("Morning, " + name);
        }
        private static void ChineseGreeting(string name)
        {
            Console.WriteLine("早上好, " + name);
        }
        //将委托类型GreetingDelegate作为形参声明
        private static void GreetPeople(string name, GreetingDelegate MakeGreeting)
        {
            //这里可以完成其他的业务逻辑
            //然后调用委托
            MakeGreeting(name);
        }
        static void Main(string[] args)
        {
             GreetPeople("hanmeimei", EnglishGreeting);//使用静态方法初始化委托
             GreetPeople("韩梅梅", new Program().ChineseGreeting);//使用实例方法初始化委托
             Console.ReadKey();
        }
    }

委托类型GreetingDelegate作为形参声明,只要是具有兼容签名和返回类型的方法都可以作为GreetPeople方法的实参。这样一来,GreetPeople方法不仅可以通过中文和英文问好了,所有与委托类型GreetingDelegate相关联的语言(方法)都可以问好了,程序更容易扩展了。

2. 委托是一种引用类型

既然委托是一种类型,应该包含类型的成员,我们将代码编译完成后,借助反编译工具看下,编译后的样子:


委托的成员

下面这两种方式是等同的:

 MakeGreeting(name);
 MakeGreeting.Invoke(name);

而BeginInvoke和EndInvoke是属于异步调用的范畴,我们稍后再说。

3.Lambda表达式比匿名方法简约

从示例1中可以看到分别显示调用了静态方法和实例方法初始化了委托,但是对于那些只使用一次的方法,就没有必要创建具名方法了。C#2.0提出了使用匿名方法代替具名方法的解决方案

  GreetingDelegate MakeGreeting = delegate (string name)
  {
     Console.WriteLine("早上好, " + name);
  };
  GreetPeople("韩梅梅", MakeGreeting);

匿名方法仍然比较繁琐,C#3.0引入了Lambda表达式

    //去掉delegate关键字并添加 =>运算符
  GreetingDelegate MakeGreeting = (string name) =>
   {
        Console.WriteLine("早上好, " + name);
   };
  GreetPeople("韩梅梅", MakeGreeting);   
  //根据委托的参数类型推断,string类型也去掉了
  GreetingDelegate MakeGreeting = name =>
  {
      Console.WriteLine("早上好, " + name);
  };
  GreetPeople("韩梅梅", MakeGreeting);         
4. 委托也是类型安全的
  GreetingDelegate MakeGreeting1 = (int name) =>
  {
      Console.WriteLine("早上好, " + name);
  };
签名不同报错
5. 泛型委托的协变和逆变

常用泛型委托

  • Action<in T,....>:有参数无返回值
  • Func<in T1,...,out TResult>:有参数有返回值,最后一个类型参数为返回值类型

关于泛型委托的协变和逆变可以阅读这篇文章《C#基本功之泛型》
有了常用泛型委托,我们就不需要自己定义GreetingDelegate委托类型了,对委托的使用进一步的简化了。

     //用泛型委托声明形参,Func于此类似,只不过有返回值而已
     private static void GreetPeople(string name,Action<string> action)
     {
            //这里可以完成其他的业务逻辑
            //然后调用委托
            action.Invoke(name);
     }
   //调用
   GreetPeople("韩梅梅", name => Console.WriteLine("早上好, " + name))

到目前为止,我们将方法的变化抽象,并用委托封装,实现了委托的简单使用。但委托还有很大的用处。
委托是类的成员,我们看一个作为类的成员使用的例子:现在生活中智能设备越来越普及,以前需要自己动手拉开窗帘、打开热水器等等,现在只需要设定场景,利用智能设备就可以完成这些操作。
当时间定格为早上7点的时候,闹钟想起,窗帘自动打开,热水器开始烧水,加湿器关闭.....

 //先不用管EventArgs参数,object 类型的sender,可以理解为任何类型都可以传递
 public delegate void EventHandler(object sender, EventArgs e);
 /// <summary>
 /// 控制中心
 /// </summary>
 public class ControlCore
  {
         public DateTime Time { get; set; } = DateTime.Now;
        /// <summary>
        /// 执行任务
        /// </summary>
        public event EventHandler Task;
 }
 static void Main(string[] args)
 {
         var controlCore= new ControlCore();
         controlCore.Task= new EventHandler(AlarmClock);
         //怎么操作委托?
         //添加、移除
         controlCore.Task+=....;
        controlCore.Task-=....;
        //或者直接覆盖掉
       controlCore.Task=....;
       controlCore.Task(null, null);
  }
  /// <summary>
  /// 闹钟
  /// </summary>
 public static void AlarmClock(object sender, EventArgs e)
  {
      Console.WriteLine("起床了,亲");
 }

作为类的成员时,要怎么操作委托?我们都知道委托还可以添加或移除方法,所以我们不仅可以直接调用委托,还可以添加、移除或者直接覆盖委托,对委托的操作没有任何限制。如此一来,破坏了类的封装性。我们希望对委托有一些限制,就像用属性去限制字段的输入输出一样;

        //将委托的访问级别改为private
        private EventHandler Task;
       //为委托添加方法
        public void AddTask(EventHandler handler)
        {
            if (this.Task== null)
                this.Task= new EventHandler(handler);
            else
                this.Task+= new EventHandler(handler);
        }
       //移除方法
        public void RemoveTask(EventHandler handler)
        {
            System.Delegate.Remove(this.Task, handler);
        }
       //因为委托定义为private,所以需要提供调用委托的接口
        public void OnTask(EventArgs e)
        {
            //7点了
            if (Time.Hour == 7)
            {
                if (this.Task!= null)
                {
                    this.Task(this, e);
                }
            }
        }


如果对委托的使用仅仅是添加或移除方法,然后执行委托的调用列表,我相信用事件会更简单容易;
事件是以委托为基础,可以理解为对委托的进一步封装。

二、对事件的理解

1. 事件是将委托封装,并对外公布了订阅和取消订阅的接口

将委托private EventHandler Task;修改为事件public event EventHandler Task;而我们创建的方法AddTask和RemoveTask也需要去掉了,重新生成代码,通过反编译工具可以看到:

重新生成

事件编译后生成的两个方法,与我们的示例中AddClick和RemoveClick方法类似;同时可以看到EventHandler委托,已经字段变为private的访问级别了(小锁表示私有);这样一来,事件帮我们完成了对委托的“限制“;
在客户端访问事件也只能+=(订阅)或者 -=(取消订阅)了,如果直接用“=“运算符赋值就会报错(在Control类内容还是可以的);


报错了
2. 事件使用 发布-订阅(publisher-subscriber) 模型

发布者:包含事件的类用于触发事件,而这个类称为事件的“发布者”。
通过声明委托类型的事件,将委托与事件关联。发布者对象调用这个事件,并通知订阅者对象
订阅者:其他注册该事件的类称为“订阅者”。
订阅者注册事件并提供事件处理程序(闹钟、打开窗帘、热水器烧水等),在发布者类中通过委托调用订阅者的事件处理程序。

   public delegate void EventHandler(object sender, EventArgs e);
  /// <summary>
  /// 控制中心
  /// </summary>
  public class ControlCore
  {
      public DateTime Time { get; set; } = DateTime.Now;
      /// <summary>
      /// 执行任务
      /// </summary>
      public event EventHandler Task;
      /// <summary>
      /// 触发事件
      /// </summary>
      /// <param name="e"></param>
      public void OnTask(EventArgs e)
      {
          //7点了
          if (Time.Hour == 7)
          {
              if (this.Task != null)
              {
                  this.Task(this, e);
              }
          }
      }
  }
      

订阅者类中的事件处理程序

        //下面的方法分别属于订阅者类中的方法,篇幅有限,没有单独声明每一个订阅者类
        /// <summary>
        /// 闹钟响起
        /// </summary>
        public static void AlarmClock(object sender, EventArgs e)
        {
            var core = (ControlCore)sender;
            Console.WriteLine(core.Time.Hour+"点了,起床了,亲");
        }
        /// <summary>
        /// 打开窗帘
        /// </summary>
        public static void OpenWindow(object sender, EventArgs e)
        {
            var core = (ControlCore)sender;
            Console.WriteLine(core.Time.Hour + "点了,打开窗帘");
        }
        /// <summary>
        /// 热水器烧水
        /// </summary>
        public static void BoilWater(object sender, EventArgs e)
        {
            var core = (ControlCore)sender;
            Console.WriteLine(core.Time.Hour + "点了,热水器开始烧水");
        }

客户端代码

        static void Main(string[] args)
        {
            var controlCore = new ControlCore();
            controlCore.Task += new EventHandler(AlarmClock);
            controlCore.Task += OpenWindow;
            controlCore.Task += BoilWater;
            while (true)
            {
                controlCore.OnTask(null);
                Console.ReadKey();
            }
        }

执行结果

7点了,起床了,亲
7点了,打开窗帘
7点了,热水器开始烧水
3. 关于sender和EventArgs

在示例代码中,可以看到将ControlCore本身作为参数传递给订阅者,既然订阅了发布者的动态,那么关于发布者的某些信息或许感兴趣(比如ControlCore中的Time)。
而EventArgs是作为发布者信息之外的信息传递

   //自定义参数类型
   public class CustomEventArgs: EventArgs
    {
        public string Arg1 { get; set; }
        public string Arg2 { get; set; }
    }

用CustomEventArgs替换委托中的参数EventArgs。public delegate void EventHandler(object sender, CustomEventArgs e)
那么在客户端调用时传入更多的信息

  controlCore.OnTask(new ButtonEventArgs()
 {
            Arg1="",
            Arg2=""
   });

总结:一直想写一篇关于委托和事件的文章,但是网上已经有很多这类优秀的文章了,不乏一些佼佼者,由浅入深,从无到有的风格将知识点讲的很透彻。如果我再按照这个类型去写,实在没有意思。
所以我想,我们可不可以从已知到未知这条路径来将知识点讲明白,比如,我们知道将具有相同属性和行为的对象抽象为类型。那么我是不是可以将具有相同签名和返回类型的方法抽象为委托?再比如,我们知道属性封装了字段,并对字段的输入输出进行了限制。那么我是不是可以将委托封装,控制委托的注册或取消,这样就引出了事件。
希望可以帮助到朋友们

:.NET关于委托和事件的编码规范

  • 委托类型的名称都应该以EventHandler结束。
  • 事件的命名为 委托去掉 EventHandler之后剩余的部分。
  • 委托的原型定义:有一个void返回值,并接受两个输入参数:一个Object 类型,一个 EventArgs类型(或继承自EventArgs)。
  • 继承自EventArgs的类型应该以EventArgs结尾。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • 本来应该学习泛型与委托的,但是发现C#这里还没有系统的记录过委托与事件,所以先打算把委托与事件补上再继续泛型与委托...
    一个有味道的名字阅读 1,476评论 1 5
  • 网上讲C#委托和事件的博文已经非常多了,其中也不乏一些深入浅出、条理清晰的文章。我之所以还是继续写,主要是借机整理...
    丑小丫大笨蛋阅读 1,122评论 0 5
  • 一、理解事件事件采用发布/订阅模型,其中发行者决定在什么情况下引发事件,而订户决定为响应事件而执行的操作。事件可以...
    CarlDonitz阅读 291评论 0 0
  • 事件 事件含义 事件由对象引发,通过我们提供的代码来处理。一个事件我们必须订阅(Subscribe)他们,订阅一个...
    天堂迈舞阅读 2,950评论 1 7
  • 亏啦饿了
    抉择_2562阅读 167评论 0 0