事件的由来
上文说到委托的安全性不佳,于是我们要将委托本身私有化,但还要暴露若干方法让外界使用。其中最重要的必然就是为委托挂接方法和调用委托,以便间接地调用委托所代表方法。那么事件event关键字就是c#提供给我们的一个语法糖。他并没有任何新的东西,只是减少了一些代码。所以,事件是一种特殊的委托,其特征有:
1 和一个(基底)委托类型合作,当声明了一个具有该基底委托类型的事件之后,用户在外部就可以为这个事件挂接方法,其方法类型必须和基底委托类型相同
2 可以将事件视为委托的方法链,使用event关键字时,该方法链是私有的,而使用委托类型名作为关键字时,该方法链是公共的
3 事件虽然是私有的,但编译器自动为我们建立了两个隐藏方法,并重载了+=和-=,使得我们可以不编写任何额外的代码,利用+=和-=操作事件上的方法,从而控制当事件触发时执行什么方法
public class Program { public static void Main() { //创建了一个新的订阅者 var c = new Car("Mycar", 0, 100); //还记得委托么,如果methodList是一个CarEngineHandler类型则可以这样写 //现在methodList是一个事件类型,这样写会报错 //事件只能出现在+=或者-=的左方,不能出现在等于号的左方 //故此时用户不能随意为其赋值,保证了安全 c.methodList = OnCarEvent1; //这是正确的 c.methodList += OnCarEvent1; //不能在外部这样调用一个事件 //c.methodList.Invoke() } public static void OnCarEvent1(string msg) { Console.WriteLine("***** message from car *****"); Console.WriteLine("=> " + msg); Console.WriteLine("****************************"); } public static void OnCarEvent2(string msg) { Console.WriteLine("=> " + msg.ToUpper()); } } public class Car { public string name { get; set; } public int currentSpeed { get; set; } public int MaxSpeed { get; set; } private bool isDead { get; set; } public delegate void CarEngineHandler(string message); public event CarEngineHandler methodList; public Car(string name, int currentSpeed, int MaxSpeed) { this.name = name; this.currentSpeed = currentSpeed; this.MaxSpeed = MaxSpeed; this.isDead = false; } public void Accel(int delta) { //死亡时执行订阅列表中的方法 if (isDead) { if (methodList != null) //在内部调用事件和调用委托没区别 methodList("Sorry, car is broken"); } else { currentSpeed += delta; if (currentSpeed >= MaxSpeed) isDead = true; else Console.WriteLine("Current speed: " + currentSpeed); } } }
事件的代码规范
微软为事件设置了代码规范,和委托相同,这些规范主要有:
1 委托名必须以Handler结尾
2 委托必须有且仅有两个参数,第一个为object类型的sender,第二个则是一个集成了EventArgs类型的自定义类类型,名字可以自定
那么我们使用代码规范来重新写一下上面的例子:
public class Program { public static void Main() { //创建了一个新的订阅者 var c = new Car("Mycar", 0, 100); c.methodList += OnCarEvent1; for (int i = 0; i < 10; i++) { c.Accel(20); } Console.ReadKey(); } public static void OnCarEvent1(object sender, CarEventArgs e) { //现在我们知道是谁触发了事件 Console.WriteLine("***** message from car: " + sender.ToString() + " *****"); Console.WriteLine("=> " + e.message); Console.WriteLine("****************************"); } } public class Car { public string name { get; set; } public int currentSpeed { get; set; } public int MaxSpeed { get; set; } private bool isDead { get; set; } //标准化的委托定义 //sender: 当发生事件时,告知订阅者是谁触发了事件 //e: 当发生事件时传递的信息(可以自定义一个类继承EventArgs类,故可以传递任意类型的信息) public delegate void CarEngineHandler(object sender, CarEventArgs e); public event CarEngineHandler methodList; public Car(string name, int currentSpeed, int MaxSpeed) { this.name = name; this.currentSpeed = currentSpeed; this.MaxSpeed = MaxSpeed; this.isDead = false; } public void Accel(int delta) { //死亡时执行订阅列表中的方法 if (isDead) { if (methodList != null) //在内部调用事件和调用委托没区别 methodList(name, new CarEventArgs { message = "Sorry, this car is dead." } ); } else { currentSpeed += delta; if (currentSpeed >= MaxSpeed) isDead = true; else Console.WriteLine("Current speed: " + currentSpeed); } } } //自定义事件发生时发送的信息格式 public class CarEventArgs : EventArgs { public string message { get; set; } }
sender和EventArgs
相信很多人在使用.net进行控件的拖拽时,如果拖拽一个按钮,然后双击它,就会发现代码多了几行(省略了无关的):
this.button1.Click += new System.EventHandler(this.button1_Click);
private void button1_Click(object sender, EventArgs e) { }
但可能不是所有人都明白那两个参数是做什么的,反正我在这个方法里写代码,然后点击那个按钮,编译器就会执行到这里面的代码。其实这正是符合微软命名规范的一个事件的例子。
1. 在初始化时,为这个对象的Click属性(注意该属性的类型是一个事件,他的基底委托是EventHandler类型的)绑定了button1_Click方法。此时如果这个事件被调用,那么就会执行button1_Click方法的代码。
EventHandler 是C#控件中最常见的委托,它没有返回类型:
public delegate void EventHandler (Object sender, EventArgs e)
我们注意到,这个委托正好和button1_Click方法签名相同,也就是说,我们可以将button1_Click方法加入到委托的方法链中。而在这里,方法链就是事件类型变量Click。
2. 当单击按钮时,经过一系列的消息轮询最终系统捕获到了你的单击动作,再经过一系列处理,最后到了Control.OnClick(System.EventArgs e)方法
3. 在这个方法里调用事件
protected virtual void OnClick(EventArgs e) { //从Events委托集合中取出名为EventClick的委托 EventHandler handler = (EventHandler)base.Events[EventClick]; //如果不为空则执行这个委托上的方法,也就是button1_Click if (handler != null) { //this就是button1 handler(this, e); } }
大略的整个过程就是:
1. 你的点击动作被windows捕获,windows把这个动作(this.button1.Click)作为系统消息发送给程序(底层消息轮询机制)
2. 程序从自己的消息队列中不断的取出消息,并在消息循环中寻找对应的处理方式
3. 对于这个消息,其sender(事件的来源)就是这个按钮,发送的消息全部在e里面,在某些事件里,e用处不大,比如在MouseEventArgs的Mouse事件中,可以看到e包括mouse的坐标值等,以供你的程序使用
4. 因为this.button1.Click挂接了方法button1_Click,故执行这里面的代码
所以说实际上当单击按钮时,其实编译器已经帮我们做了很多事情,例如委托的建立,事件的挂接和执行,这些都已经被封装到你根本就不需要知道也可以编写出来成功执行的代码的程度了。那么委托和事件这个话题差不多就结束了(在c#1.0这个层面上)。在更高版本的c#中,委托和事件被包装的更加简便易用了,基于泛型的委托action和func的出现,更是(基本上)完全替代了原生的委托delegate。这些精彩的内容当然要等待到介绍c#2和3的时候一块讲述。
参考资料
http://www.cnblogs.com/leslies2/archive/2012/03/22/2389318.html
http://www.tracefact.net/csharp-programming/delegates-and-events-in-csharp.aspx
http://www.cnblogs.com/zhili/archive/2012/10/29/ButtonClickEvent.html