看看这个经常被0基础程序猿弄不懂的 “事件”
众所周知在面试中。常常有些崽子面试官会问些“事件和托付”的关系,或许一路走来的程序猿大多都会被问到这个。那么对于这个
高频的”事件和托付“问题,怎样回击呢?首先我从最经典的一套面试题说起,用事件来实现 “猫爪老鼠“,这是一个从网上copy过来的一
个样例。
static void Main(string[] args) { Mouse mouse = new Mouse(); Cat cat = new Cat(); cat.OnCry(); Console.ReadLine(); } } public delegate void CryEventHandler(); public class Cat { public static event CryEventHandler Cry; public Cat() { Console.WriteLine("Cat:I'm coming."); } public virtual void OnCry() { Console.WriteLine("Cat:MiaoMiao"); if (Cry != null) { Cry.Invoke(); } } } public class Mouse { public Mouse() { Cat.Cry += new CryEventHandler(Run); Console.WriteLine("Mouse:I go to find something,and I must always listen cat's crying."); } public void Run() { Console.WriteLine("Mouse:A cat is coming,I must go back!"); } }
事件定义啥的什么玩意这个我就不说了。没什么意思。为了了解这个跟托付有什么关系,以下我们来看看这段代码最后生成的IL是什么样的。
1:CryEventHandler托付
1 public delegate void CryEventHandler();
这个我想大家都清楚,托付本质上是一个继承于MulticastDelegate的类,同一时候会生成仅有的4个方法,看下IL即知。
2:Cat类
1 public class Cat 2 { 3 public static event CryEventHandler Cry; 4 5 public Cat() 6 { 7 Console.WriteLine("Cat:I'm coming."); 8 } 9 10 public virtual void OnCry() 11 { 12 Console.WriteLine("Cat:MiaoMiao"); 13 if (Cry != null) 14 { 15 Cry.Invoke(); 16 } 17 } 18 }
从这个类中,我们看到了一个Cry事件,然后就是一个Cry.Invoke()。只是当你看到Invoke的时候,你是不是非常怀疑Cry是不是一个托付字段呢?
事实上你怀疑的是一点问题都没有,32个赞,看下IL。
从上图中我们看到了两个好玩的东西:
① field Cry 字段,完整定义例如以下,然来所谓的“事件字段” 事实上在IL以下蜕变成了托付字段,假设你认为非常奇怪,请看第二点。
.field private static class Sample.CryEventHandler Cry
② add_Cry,remove_Cry,假设只将事件字段变成托付字段,那确实是编译器在发什么神经。然来编译器还给事件配备了两个方法,这个
事实上也就是事件里面+=。-=的奥秘所在,以下我们挑add_Cry方法说下,看看方法定义的IL代码是怎么样的。
非常新鲜,我们找到了Combine方法。这个我们都知道。原来事件中的+=,事实上就是利用Combine来将当前的托付实例放到Delegate的
托付链表中(事实上里面是array实现的),为了方便理解,我把上面的IL代码翻译成C#代码。
1 public class Cat 2 { 3 /// <summary> 4 /// 私有的托付变量 5 /// </summary> 6 private static CryEventHandler Cry; 7 8 /// <summary> 9 /// 事件生成的方法 10 /// </summary> 11 /// <param name="cryEventHandler"></param> 12 public void Add_Cry(CryEventHandler cryEventHandler) 13 { 14 var result = (CryEventHandler)Delegate.Combine(Cry, cryEventHandler); 15 16 Interlocked.CompareExchange<CryEventHandler>(ref Cry, result, Cry); 17 } 18 19 public void Remove_Cry(CryEventHandler cryEventHandler) 20 { 21 var result = (CryEventHandler)Delegate.Remove(Cry, cryEventHandler); 22 23 Interlocked.CompareExchange<CryEventHandler>(ref Cry, result, Cry); 24 } 25 26 public Cat() 27 { 28 Console.WriteLine("Cat:I'm coming."); 29 } 30 31 public virtual void OnCry() 32 { 33 Console.WriteLine("Cat:MiaoMiao"); 34 35 if (Cry != null) 36 { 37 //托付专用的四个方法。invoke,begininvoke,endinvoke,ctor 38 Cry.Invoke(); 39 } 40 } 41 }
可能有些同学对IL指令不是非常熟悉。没关系,我也一样,咱博客园上面有位大神飞鸟的一篇IL指令集的博文也许能帮得到你。
3:Mouse类
假设你对Cat类的IL代码琢磨的几乎相同的话。以下这个Mouse类就很easy了,只调用而已嘛。
1 public class Mouse 2 { 3 public Mouse() 4 { 5 Cat.Cry += new CryEventHandler(Run); 6 Console.WriteLine("Mouse:I go to find something,and I must always listen cat's crying."); 7 } 8 9 public void Run() 10 { 11 Console.WriteLine("Mouse:A cat is coming,I must go back!"); 12 } 13 }
这个地方最让人关心的就是:Cat.Cry += new CryEventHandler(Run) 这个语句,从它的IL中能够看到事实上做了三件事。
① ldftn: 将Run方法的非托管指针推送到计算堆栈上。
② newobj: 将CryEventHandler托付new一下,同一时候将计算堆栈上的Run方法的非托管指针作为构造函数的參数。
③ call: 调用Cat类的Add_Cry方法。将CryEventHandler的实例作为參数传递下去。
以下继续将该IL代码反编译回来,只是针对IL指令:call void Sample.Cat::add_Cry(class Sample.CryEventHandler)
并没有非常好的翻译过来,仅仅能new Cat()了一下才干调用Add_Cry,从而触发了Cat的构造函数。
1 public class Mouse 2 { 3 public Mouse() 4 { 5 var cryHandler = new CryEventHandler(Run); 6 7 /* 8 * 针对IL:call void Sample.Cat::add_Cry(class Sample.CryEventHandler) 9 * 这个没有反编译好,由于我new Cat()将会再次调用构造函数。 10 */ 11 new Cat().Add_Cry(cryHandler); 12 13 Console.WriteLine("Mouse:I go to find something,and I must always listen cat's crying."); 14 } 15 16 public void Run() 17 { 18 Console.WriteLine("Mouse:A cat is coming,I must go back!"); 19 } 20 }
好了,说了这么多。应该也有总结性的东西出来了,原来事件是完全然全的建立在托付的基础上。你能够觉得事件就是用托付来玩一个
观察者模式的,你甚至能够觉得事件就是托付。没有本质差别。