bindsang

工作五年,长期从事于asp.net方面的编程,业余爱好VC编程,温和、谦虚、自律、自信、善于与人交往沟通
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

DOTNET事件拾遗

Posted on 2008-07-28 16:13    阅读(416)  评论(0编辑  收藏  举报

    使用C#也好几年了,从一开始的囫囵接收,到后来积累到一定程度后再回过头去细细思考,这期间也发现了一些恍然大悟,矛塞顿开的情况.我.NET的事件触发的原理的认识就是这样走过来的.

    在刚使用C#的时候(vs2003),没有多少参考资料,没有Reflector,只有一个随着IDE一起的MSDN,所有的东东基本上都需要从那上去去找,那个时候对.NET里面的事件一直有几个不明白的地方:

有这么一段代码:

 

 1class MyClass
 2    {
 3
 4        public event MyDelegate MyEvent;
 5    }

 6    class MyClassTest
 7    {
 8        public void DoSomething()
 9        {
10            MyClass c = new MyClass();
11            c.MyEvent += new MyDelegate(this.TestEventFunc);
12        }

13    }

14
15    public delegate void MyDelegate(object sender, EventArgs e);

 

1. 事件本质上是一个委托,为什么在声明的时候没有给事件初始化,但是挂接事件的时候却可以直接用+=给事件增加一个处理函数,这个时候却不会报NullReference异常,而单独声明一个委托引用直接这样使却会报错?

2  为什么基类明的事件,派生类无法直接触发,就像下面这样:

 

 1class A
 2{
 3   public event MyDelegate MyEvent;
 4}

 5public delegate void MyDelegate(object sender, EventArgs e);
 6
 7class B :A
 8{
 9   public void DoSomeThing()
10   {
11      
12      if(MyEvent != null)  // 这个地方会出错
13          MyEvent(this, EventArgs.Empty);  // 这个地方会出错
14   }

15}

 

3. 为什么在Framework中,对于一个没有密封(sealed)的类,每个声明的事件基本上都有一个On+事件名称的protected的函数(有时候还是虚函数)?

 

其实这三个问题在后来看了用Reflector反编译的代码后也就渐渐清楚了.

首先我们来看看class A中的MyEvent事件生成的代码反编译的结果

 

 1public event MyDelegate MyEvent
 2{
 3    add
 4    {
 5        MyEvent = (MyDelegate)Delegate.Combine(MyEvent, value);
 6    }

 7    remove
 8    {
 9        MyEvent = (MyDelegate)Delegate.Remove(MyEvent, value);
10    }

11}

12private MyDelegate MyEvent;

    怎么和我们刚才声明的事件有很大的不一样?原来这都是编译器在中间做了手脚,加入了一些必要的代码,让我们只用书写很简洁,很优雅的代码就可以完成想要做的事件,在现在的C#3.0里面更是充分的展现了这一点,比如说匿名函数,比如说匿名对象等.

    看看编译后的代码,里面主要有一块类似于Property的结块,和一个普通的与事件具有相同委托类型的私有成员,而个成员的名称恰恰与原来事件的名称一样(这个我想也只有编译器才能把他俩的名字弄成一样的了,呵呵)..之所以说与Property类似,是因为Property里面的get,set关键字被这里的add,remove替代了,其实这里的add,remove与其中的vlaue都是C#中的上下文关键字,只在特定的上下文中生效,在这里就是事件的声明中.其实使用add和remove这种显示的添加和移除事件的方式不是什么秘密了,在IDE里面这样写的代码出是合法的,只是平时我们太过专注于用简洁的方式来使用事件了,所以对这种方式知道的人不是太多.而现在我们只是在剥去表现的现象还原本质罢了.

    从上面的代码我们可以看出,挂接/移除一个事件的时候,最终并不是直接用事件本身去作用在+=或-=运算符上,而只是借用了一下这个运算符的形式上的意思,来表达是在增加或移除一个事件委托(也就是增加或移除事件函数).其内部是通过Delegate类的两个静态方法来实现的,而这两个方法的要求仅仅是不能都为null,这样就能够解释第一个问题,为什么事件没有经过初始化就能够使用+=或-=运算符.这也间接说明了,如果我们人为的给事件加上了初始化的步骤,类似于MyEvent = new MyDelegate(this.TestEventFunc);反而会出问题,因为对于编译器来说不知道应该把赋值运算符"="分配到add/remove块的那一个上面.而且只使用+=或-=运算符的话,就排除了使用之前还得先判断一下是否为null这样的操作.

    同样从给出的这段代码里面我们也能解释第二个问题了,当我们在派生类里使用

 

12      if(MyEvent != null)  // 这个地方会出错
13          MyEvent(this, EventArgs.Empty);  // 这个地方会出错

 

 这样的代码的时候,我们习惯性的认为这里的MyEvent就是声明的事件本身,因为只有先声明过的才能使用嘛,但是在派生类里即又不能这样写,这才会有上面的疑惑.其实这里的MyEvent到底代表的是什么,在调式器里很容易就看得清楚

不知道大家有没有注意到上图中的MyEvent的图标代表了这是一个成员变量而不是一个事件,前面的一把锁也说明了这是个私有成员而不是公有的成员.类型不用说了自然是MyDelegate委托类型,这充分说明了现在使用的正是编译器给我们生成的那个与事件同名的委托类型的私有成员,因为是私有的成员嘛,当然不可能被派生类访问到了,因此这也解释了为什么在派生类里面这样使用就出错这个问题.其实我们自己完全可以用add/remove这样的形式写出自己的事件,只不过这时候用到的事件代理就不能是与事件名称同名的了,因为一切都得靠自己,所以编译器也不帮你了.顺便说一句,add/remove形式声明的事件中,add/remove必须同时出现,不能只出现其中之一.

这里我们把事件和引发事件的委托两个名字分开了,这样就看得就更清楚了

 

明白了第二个问题之后,第三个问题也就迎刃而解了.很多时候基类声明的事件派生类也需要在一些方法里引发这些事件,但是又没有办法直接引用基类的那个私有的成委托成员,怎么办呢?既然派类不能直接引用的话,那么我们就"曲线救国",用间接的方式调用,达到触发事件的目的,派生类不是不能调用吗?但是同一个类里的函数是可以访问到的,那么我在派生类里再去调这个函数不就行了么,而且这个函数把判断委托是否为空的逻辑也包括进去了,不必要在每个引发事件的地方都去判断,因此本类里的代码(声明事件的类)就算是可以直接访问这个委托,也最好不要直接访问,而改用事件函数去访问.一般来说,这个函数只是为了引发事件而产生的,所以在命名就写成了On+事件名称这样的形式,大多数情况只是对本类和派生类有用,因此访问修饰符采用protected是比较合理的,而在引发事件之前和之后,派生类有可能需要做一些自己的事情,这样就需要覆盖(override)基类的方法,所以多数时候是声明成virtual的.这些仅仅是为了更好的在派生类里使用事件而约定的,M$自己最先这么做了,大家都是C#的粉丝,当然也只好追随着去了.

 

感觉事件的这种写法有点类似于C#3.0里面的"对象构造者",只用声明一个Property编译器就能自己生成背后隐藏的那个私有成员变量.因为事件声明中add/remove必须同时出现,所以连"对象构造者"中类似的get/set声明都被省掉,形式上更简洁.可是省掉后怎么分区这是一个事件声明的简化而不是一个普通的委托成员呢,"那就在前面加个前键字吧",于是event关键字就这样产生了(呵呵,仅仅是个人YY一下,不作标准的),没准C#下一版的时候会把get/set全都有的Property的后面的那个{get;set;}声明也去掉,而在前面加上一个property 之类的关键字也说不定(扯远了,呵呵).