预备知识
在学习委托和事件之前,我们需要知道的是,很多程序都有一个共同的需求,即当一个特定的程序事件发生时,程序的其他部分可以得到该事件已经发生的通知。
而发布者/订阅者模式可以满足这种需求。简单来说,在这种模式中,发布者定义了一系列程序的其他部分可能感兴趣的事件。其他类可以“注册”,以便再这些事件发生时发布者可以通知它们。这些订阅者类通过向发布者提供一个方法来“注册”以获取通知。当事件发生时,发布者“触发事件”,然后执行订阅者提交的所有事件。
注:由订阅者提供的方法称为回调方法,或者说回调函数,关于回调函数可以参考https://www.zhihu.com/question/1980113 。
大概了解了下基本机制后,我们再来看看委托和事件:
什么是委托?
委托和类一样,是一种用户自定义的类型。但类表示的是数据和方法的集合,而委托则持有一个或多个方法,以及一系列预定义操作(你可以暂时把它理解成一个类型安全的C++函数指针)。可以通过以下操作来使用委托:
1)声明一个委托类型(类似方法声明,但委托没有实现块)
public delegate void MyDel(int x); //格式为[修饰符] [委托关键字delegate] [返回类型][委托名][签名]
2)使用该委托类型声明一个委托变量。
MyDel delVar;
3)创建委托类型的对象,把它赋值给委托对象。新的委托对象包括指向某个方法的引用,这个方法和第一步的签名和返回类型一致。
创建委托对象可以这样:
delVar = new MyDel(SomeClass.Method); //创建委托并保存第一个个方法的引用
或者使用快捷语法,比如这样:
delVar = SomeClass.Method;
4 )也可以为委托进行赋值,但这样旧的委托引用会被GC回收掉。
delVar = new MyDel(SomeClass.Method1);//创建一个新委托对象 delVar = OtherClass.Method2; //赋值后,之前的Method1的引用被Method2覆盖掉了
5)为委托对象增加或移除其他的方法
//增加方法 delVar += someMethod1; delVar += someMethod2; //移除方法 delVar -= someMethod1;
6)组合委托。可以将两个委托组合生成成一个新的委托,这个新的委托的调用列表连接了其他两个委托的调用列表副本。
MyDel delA = someClass.Method1; MyDel delB = otherClass.Method2; MyDel delC = delA + delB;
7)调用委托。调用委托跟调用函数方法一样,参数必须同其调用列表中的方法一致。调用委托时会执行它的调用列表中的所有方法。(注:调用时委托不能为空)
delVar(Parameters);
其他注意事项:1.调用带返回值的委托时,委托的返回值为其调用列表中最后一个方法的返回值。2.调用带引用参数的委托时,参数会根据调用列表中的一个或多个方法的返回值二改变。
什么是事件?
一句话概括,事件就好像是专门用于某种特殊用途的简单委托的封装。或者说,事件包含了一个私有的委托(也就是说你无法直接访问事件的委托)。
另外,事件中可用的操作比委托要少,对于事件我们只可以添加、删除或调用事件处理程序。事件被触发时,它调用对应的委托来依次调用调用列表中的方法。
对于事件的使用,.Net框架提供了一种标准模式,它定义了一种标准的委托类型EventHandler。
EventHandler声明如下:
public delegate void EventHandler(object sender,EventArgs e)
// sender保存的是触发事件的引用,因为是object,所以可以匹配任何类型的实例 (可以使用as运算符拆箱转换)
// EventArgs是所有事件信息的基类,你可以自己声明一个继承自EventArgs的事件信息类来保存一些需要传递的数据信息。
声明EventHandler对应的事件:
public event EventHandler someEvent;
注:你可以按照 delegate void XXXXHandler(object sender,EventArgs e)的格式声明自定义的标准委托类型。
增加移除事件处理程序和委托类似,这里就不再赘述。
说了这么多,最好亲自动手实践一下,这样才能巩固自己学习的知识。
一个小小的应用例子
LOL是现在许多人(包括我)十分喜爱的一款很流行的5v5多人游戏,这个游戏里面的一方中5个人按职责划分为上单、打野、中单、ADC、辅助。这里主要是通过打野和上单的一个简单互动来说明一下事件的触发机制。具体的代码如下:
1 using System; 2 using System.Threading; 3 4 namespace EventStudy 5 { 6 7 8 9 public class Top //事件发布者(上单) 10 { 11 public string Hero { get; set; } //使用的英雄 12 public int hp { get; set; } = 500; //英雄生命值 13 public delegate void LowHphandler(object sender, LowHpEventArgs e); //声明一个微软标准类型的委托 14 public event LowHphandler LowHpEvent; //声明一个该委托类型的事件 15 16 public class LowHpEventArgs : EventArgs //低生命事件信息 17 { 18 public readonly string LowHpHero; 19 public int restHp { get; set; } 20 public LowHpEventArgs(string hero, int _resthp) 21 { 22 LowHpHero = hero; 23 restHp = _resthp; 24 } 25 } 26 27 public void BackToBase() //回城补给 28 { 29 Console.WriteLine("{0}回城了",Hero); 30 hp = 500; 31 Thread.Sleep(3000); 32 Console.WriteLine("他传送回了上路"); 33 } 34 35 public Top(string hero) //构造函数 36 { 37 Hero = hero; 38 } 39 40 41 public void fight(int battleCounts )//战斗 battleCounts = 战斗次数 42 { 43 for(int i = 0; i < battleCounts;i++) //战斗流程 44 { 45 Console.WriteLine("我方上单{0}正在与对方上单激烈对线中......",Hero); 46 Thread.Sleep(2000); 47 hp -= 100; 48 Console.WriteLine("交战后剩余生命值:{0}", hp); 49 Thread.Sleep(1000); 50 if(hp <= 100) //当生命值小于等于100时 51 { 52 if (LowHpEvent != null) //如果有对象注册 53 LowHpEvent(this, new LowHpEventArgs(Hero, hp)); //触发事件 54 else 55 Console.WriteLine("{0}说:草,没人关注上路啊?",Hero); 56 break; 57 } 58 } 59 } 60 61 } 62 63 class Jungle //订阅者(打野) 64 { 65 public string HeroName; //使用的英雄名字 66 public Jungle(string name)//构造函数 67 { 68 HeroName = name; 69 } 70 public void Help(object sender,Top.LowHpEventArgs e)//回调函数,或者说事件处理程序 71 { 72 Console.WriteLine("{0}说:卧槽,我们的上单{1}只有:{2}血了,我得去帮他",HeroName, e.LowHpHero, e.restHp); 73 } 74 } 75 76 77 78 class Program 79 { 80 static void Main(string[] args) 81 { 82 //创建一个上单和一个打野 83 Top Yassuo = new Top("疾风剑豪-亚索"); 84 Jungle Leesin = new Jungle("盲僧-李青"); 85 86 Yassuo.LowHpEvent += Leesin.Help; //李青开始关注着亚索的生命情况 87 Yassuo.fight(4); //亚索开始和对面对线 当达到特定情况时触发事件,然后执行对应的委托(即回调已经注册了的李青的Help方法) 88 Yassuo.BackToBase(); //亚索血量过低回城了 重置hp为500 89 Yassuo.LowHpEvent -= Leesin.Help; //亚索回城了,李青不再关注 90 Yassuo.fight(4); //亚索继续和对面对线 当达到特定情况时又触发事件,但李青已经取消关注,事件处理为空,所以直接打印亚索要说的话 91 Console.ReadLine(); //Pause 92 } 93 } 94 }
对应的控制台输出如下:
这里概括来说主要就是盲僧的Help方法注册了亚索的低血量事件lowHpEvent,当亚索低血量时触发了事件,然后通过相应的的委托回调了盲僧的Help方法。亚索回城后,盲仔取消了对亚索低血量事件的关注。所以之后亚索再次低血量时,因为没有对应的事件处理程序(LowHpEvent == null),所以亚索开始吐槽没人来上路。
参考的相关资料:
C#图解教程(第四版) 作者:[美]Daniel M.Solis
大白话系列之C#委托与事件 :http://www.cnblogs.com/wudiwushen/archive/2010/04/20/1703368.html 作者:波哥2010
知乎回调函数相关: https://www.zhihu.com/question/1980113 。