事件和委托我已经是至少第3次理解了. 以前一直似懂非懂, 前两次专门抽了一整天的时间来看事件和委托. 今天有幸听了张高峰老师的事件委托, 突然茅塞顿开, 据说老张当年被公司委派到西雅图微软专门学习了8个月的.NET(Version 1.1), 不是盖的呀. 言归正传
什么是事件? 什么是委托呢?
简单讲, 事件就是委托类型的变量(简称委托变量), 委托就是个类, 一个用来封装方法的特殊类.
我们非常熟悉的代码(在WinForm程序额设计资源代码中):
this.button1.click += new System.EventHandler(this.button1.click);
如果我们用非常简单非常直观的理解去看这行代码:
new是用来创建对象的, 创建什么对象呢? 创建了一个System.EventHandler类型的对象, 括号里是变量自身的引用做参数. 如果换一种类似的写法, Class C = new Class(), 是不是一眼就看出来了? 上面+=后边的内容就是个构造函数, 通过new创建了System.EventHandler的对象. 为什么是+=呢, 最后再说.
还看第一行代码, this.button1明显是Button类的对象, 那么我们尝试还原Button类中的仅关于这段代码的定义:
public class Button
{
public EventHandler click; //定义了一个System.EvenHandler类型的变量click
……
……
}
如果我们new一个Button的对象button1,然后找到其中的数据成员click, 便得到:
this.button1.click = new System.EventHandler(); //用new调用EventHandler的构造函数创建了它的一个对象.
是不是和第一行代码有点类似? 事实上EventHandler就是个委托, 是个特殊的类. 如果把Button类中的代码改成 public event EventHandler click; 就成了书上写的事件的定义, 为什么要加event, event只是对这个变量做了些限制.
委托是个类, 为什么Microsoft会创建委托这个特殊的类呢?
指针, 这只能怪指针的太灵活, 太高效, 太方便了. C和C++是支持指针的, 虽然对程序员有很高的要求, 但是其高效额特点, 而且在某些情况下必须使用指针才能实现功能. 例如: 庞大的Windows操作系统就大量的使用了指针.
有书上说, C#不支持指针是绝对错误的, C#支持指针, 只不过是很有限的指针, 在C#中只支持13中简单类型的指针: 8种整型(byte,short,int,long) * 2, 3种实型(float,double,decimal), 布尔, 字符. 在C#和Java都不允许程序员直接操作指针(地址).
我们通常所说的方法或者函数, 都是引用型的变量, 其内部盛放的其实就是个地址, 即指针.
由此产生了个矛盾: C#不允许传递指针, 那么也就无法向C++那样传递函数或者方法. 但是C#又希望能够使用这种高效灵活的机制, 于是Microsoft就创建了一个很特殊的类, 用来包装(封装)方法, 我们通过传递该类的对象来达到传递方法的目的. 就好比, 小朋友喜欢打游戏机, 但是手拿着游戏机的话, 老师不让进校园. 那么拿个书包包装(封装)一下, 于是就进去校园了. 传递方法(指针)毕竟是个高难度或者说危险的事情, 因此C#只允许在一个很特殊的类的构造函数中传递方法(指针), 这个特殊的类就是委托.
我们来梳理一下:
委托 à 特殊类 à 该类用来包装函数指针(方法名) à 通过传递对象而达到传递方法的目的
简言之, 用委托类的对象包装方法后, 就可以传递方法(函数).
方法是存在于类中的, 如果要把方法封装到本类之外的另一个类中, 通过另外一个类的对象来调用方法(多肽), 可以采用继承来实现, 即:
public class 类名 : Delegate
{
public 返回值 方法名(参数列表); //如: public void A(int i, string s, …);
}
方法是存在于类中的, 当我们需要传递一个方法时, 我们需要首先定义一个类派生自Delegate, 然后再在类中书写要传递的方法. 如果每次传递方法都要这么做的话, 是在很麻烦, 因此Microsoft对上面的代码做了简化, 既然Delegate是个类, 那么类的定义就简化为:
public delegate 类名 //将类Delegate变成关键字delegate来声明派生于委托的类
委托的目的是为了传递方法, 那么如何区别不同的方法呢? 所有方法都有返回值、方法名及参数列表, 只要返回值和参数列表相同, 那么他们就是同类的方法. 即便如此, 还是会有很多不同类型的方法, 因此直接将某类型的方法直接绑定到委托的定义上. 经过再次变化后得到了现在的委托的定义
public delegate 返回值 类名( 参数列表 ) ; //这里的类名即是委托类
再来看我们的需求, 传递方法(函数)的需求 -> 使用委托类包装函数 -> 使用委托
我们使用委托类包装方法, 通过委托类的对象传递某种类型的方法, C#规定有且只有在委托类的构造函数中才能传递方法(函数指针), 于是有:
委托类型 x = new 委托(方法名); //x是委托变量, 指向堆中的一个委托对象, 通常将x称为委托对象.
那我们再看最开始的那段代码:
public class Button
{
public event EventHandler click; //定义了一个System.EvenHandler类型的变量click
……
……
}
如果,给类中的委托变量加个event修饰符, 用来对委托对象做些限制, 那么我们要传递一个方法是不是可以写成:
this.button1.click += new System.Eventhandler(方法名); //通过构造函数将方法包装到委托对象中, 所以说事件是本身就是委托变量.
由于事件中存放的是引用, 该引用指向堆中的一个委托对象, 所以通常也可以说事件就是委托对象. 所以: event + 委托变量 = 事件.
前面说过, 方法或者函数本身存放的只是个地址的引用, 包装到委托类中后, 委托变量中实际存放的也只是个地址的引用. 而C#不允许我们直接操作地址, 所以肯定无法将方法从委托对象中抽离出来, 因为抽离出来的方法是个地址.
也就是说我们可以将方法通过委托的构造函数包装起来, 却不可以在去掉包装拆出里面的方法, 那么如何调用方法呢? 这时微软又出面了, 我们可以直接使用委托变量名调用方法, 直接在委托变量名后跟方法列表即可, 如: 委托变量名(参数列表).
如此一来, 委托变量就可以代表一个方法, 而实际上在Button类内部的click变量代表的那个方法被定义成虚方法:
public class Button
{
virtual void Button.click(Eventargs e) //我们可以用override改写这个button.click方法
{
if(click != null)
{
click(this,e); //到这里执行具体的事件(委托变量)上包装的那个方法,
}
}
}
this表示调用委托对象包装的那个方法的地方, 即事件发起方---事件源, e表示与windows相关的消息参数(后\边解释).
我们看这个按钮如何执行:
操作系统捕获鼠标, 获得鼠标的位置和鼠标的操作, 如果发现:
1. 鼠标的位置在Button之上
2. 鼠标点了左键
当以上两个条件同时满足时, 将调用Button类内部的一个方法Button.click方法, 由该方法调用委托变量包装的方法, 即事件方法click(this,e). 如果click事件(委托变量)为空, 则什么也不做, 否则执行其上包装的方法.
最后看下EventArgs e 和为什么是+=?
EventArgs直译就是事件参数, 它其实是Windows的消息(Windows是基于消息机制的), 当我们在屏幕上点击鼠标操作时, windows会把鼠标的位置, 鼠标的操作以及相关信息封装在一个称为EventArgs的类中, 然后传给委托对象绑定的方法, 也就是事件名(参数列表), this表示事件源, EventArgs记录Windows上相关消息的信息.
为什么是+=呢? click事件是委托变量, 里面存放了委托对象, 当我们用=时, 其实是将另外一个委托对象的引用赋给了click事件, 这样我们任意时刻只能调用一种方法. 而事件发生时, 我们通常希望其执行一连串的响应, 用=将覆盖上次绑定的方法, 所以用+=. +=操作执行过程是: 首先找到click事件(委托变量)上的委托对象, 判断委托对象的next字段是否为空, 若为空则将新的委托对象的引用放在next字段之后, 否则不为空时, 将通过next字段找到下一个委托对象, 判断下一个委托对象的next字段, 直到找到空的next字段, 将自身的引用赋予空的next字段, 我们称这个链接的链表结构为委托链.
对于委托变量封装的几个方法, 我们至少可以通过四种方式来使用绑定方法:
1. 集合结构, 如: 哈希表, 荣国哈希表来执行多个绑定方法
2.多肽, 通过调用基类上的方法, 也能调用不同的绑定方法.
3.委托变量(事件也可, 事件也是委托变量, 但是有些限制, 不如直接用委托变量灵活), 通过委托变量(方法列表)调用绑定方法.
4. 通过配置文件也能使用绑定的方法.