c 委托和事件件,求详细代码和过程

你可能不知道的陷阱:C#委托和事件的困惑 - 文章 - 伯乐在线
& 你可能不知道的陷阱:C#委托和事件的困惑
一. 问题引入
通常,一个C语言学习者登堂入室的标志就是学会使用了指针,而成为高手的标志又是“玩转指针”。指针是如此奇妙,通过一个地址,可以指向一个数,结构体,对象,甚至函数。最后的一种函数,我们称之为“函数指针”(和“指针函数”可不一样!)就像如下的代码:
int func(int x); /* 声明一个函数 */
int (*f) (int x); /* 声明一个函数指针 */
f= /* 将func函数的首地址赋给指针f */
int func(int x); /* 声明一个函数 */&&&&int (*f) (int x); /* 声明一个函数指针 */&& f=func; /* 将func函数的首地址赋给指针f */
C语言因为函数指针获得了极强的动态性,因为你可以通过给函数指针赋值并动态改变其行为,我曾在单片机上写的一个小系统中,任务调度机制玩的就是函数指针。
在.NET时代,函数指针有了更安全更优雅的包装,就是委托。而事件,则是为了限制委托灵活性引入的新“委托”(之所以为什么限制,后面会谈到)。同样,熟练掌握委托和事件,也是C#登堂入室的标志。有了事件,大大简化了编程,类库变得前所未有的开放,消息传递变得更加简单,任何熟悉事件的人一定都深有体会。
但你也知道,指针强大,高性能,带来的就是危险,你不知道这个指针是否安全,出了问题,非常难于调试。事件和委托这么好,可是当你写了很多代码,完成大型系统时,心里是不是总觉得怪怪的?有当年使用指针时类似的感觉?
如果是的话,请看如下的问题:
若多次添加同一个事件处理函数时,触发时处理函数是否也会多次触发?
若添加了一个事件处理函数,却执行了两次或多次”取消事件“,是否会报错?
如何认定两个事件处理函数是一样的? 如果是匿名函数呢?
如果不手动删除事件函数,系统会帮我们回收吗?
在多线程环境下,挂接事件时和对象创建所在的线程不同,那事件处理函数中的代码将在哪个线程中执行?
当代码的层次复杂时,开放委托和事件是不是会带来更大的麻烦?
列下这些问题,下面就让我们讨论这些”尖酸刻薄“的问题。
二. 事件订阅和取消问题
我们考虑一个典型的例子:加热器,加热器内部加热,在达到温度后通知外界”加热已经完成“。 尝试写下如下测试类:
/// 热水器
public class Heater
public event EventHandler OnB
void RasieBoiledEvent()
if(OnBoiled==null)
Console.WriteLine("加热完成处理订阅事件为空");
OnBoiled(this, new EventArgs());
private Thread heatT
public void Begin()
heatTime = 5;
heatThread = new Thread(new ThreadStart(Heat));
heatThread.Start();
Console.WriteLine("加热器已经开启", heatTime);
private int heatT
private void Heat()
while (true)
Console.WriteLine("加热还需{0}秒", heatTime);
if (heatTime == 0)
RasieBoiledEvent();
heatTime--;
Thread.Sleep(1000);
<div class="crayon-num" data-line="crayon-577b3<div class="crayon-num crayon-striped-num" data-line="crayon-577b3<div class="crayon-num" data-line="crayon-577b3<div class="crayon-num crayon-striped-num" data-line="crayon-577b3<div class="crayon-num" data-line="crayon-577b3<div class="crayon-num crayon-striped-num" data-line="crayon-577b3<div class="crayon-num" data-line="crayon-577b3<div class="crayon-num crayon-striped-num" data-line="crayon-577b3<div class="crayon-num" data-line="crayon-577b3<div class="crayon-num crayon-striped-num" data-line="crayon-577b3<div class="crayon-num" data-line="crayon-577b3<div class="crayon-num crayon-striped-num" data-line="crayon-577b3<div class="crayon-num" data-line="crayon-577b3<div class="crayon-num crayon-striped-num" data-line="crayon-577b3<div class="crayon-num" data-line="crayon-577b3<div class="crayon-num crayon-striped-num" data-line="crayon-577b3<div class="crayon-num" data-line="crayon-577b3<div class="crayon-num crayon-striped-num" data-line="crayon-577b3<div class="crayon-num" data-line="crayon-577b3<div class="crayon-num crayon-striped-num" data-line="crayon-577b3<div class="crayon-num" data-line="crayon-577b3<div class="crayon-num crayon-striped-num" data-line="crayon-577b3<div class="crayon-num" data-line="crayon-577b3<div class="crayon-num crayon-striped-num" data-line="crayon-577b3<div class="crayon-num" data-line="crayon-577b3<div class="crayon-num crayon-striped-num" data-line="crayon-577b3<div class="crayon-num" data-line="crayon-577b3<div class="crayon-num crayon-striped-num" data-line="crayon-577b3<div class="crayon-num" data-line="crayon-577b3<div class="crayon-num crayon-striped-num" data-line="crayon-577b3<div class="crayon-num" data-line="crayon-577b3<div class="crayon-num crayon-striped-num" data-line="crayon-577b3<div class="crayon-num" data-line="crayon-577b3<div class="crayon-num crayon-striped-num" data-line="crayon-577b3<div class="crayon-num" data-line="crayon-577b3<div class="crayon-num crayon-striped-num" data-line="crayon-577b3<div class="crayon-num" data-line="crayon-577b3<div class="crayon-num crayon-striped-num" data-line="crayon-577b3<div class="crayon-num" data-line="crayon-577b3<div class="crayon-num crayon-striped-num" data-line="crayon-577b3<div class="crayon-num" data-line="crayon-577b3<div class="crayon-num crayon-striped-num" data-line="crayon-577b3<div class="crayon-num" data-line="crayon-577b3<div class="crayon-num crayon-striped-num" data-line="crayon-577b3
/// && /// 热水器&& /// && public class Heater&& {&&&&&& public event EventHandler OnBoiled;&&&&&& private&&void RasieBoiledEvent()&&&&&& {&&&&&&&&&& if(OnBoiled==null)&&&&&&&&&& {&&&&&&&&&&&&&& Console.WriteLine("加热完成处理订阅事件为空");&&&&&&&&&& }&&&&&&&&&& else&&&&&&&&&& {&&&&&&&&&&&&&& OnBoiled(this, new EventArgs());&&&&&&&&&& }&&&&&& }&&&&&& private Thread heatThread;&&&&&& public void Begin()&&&&&& {&&&&&&&&&& heatTime = 5;&&&&&&&&&& heatThread = new Thread(new ThreadStart(Heat));&&&&&&&&&& heatThread.Start();&&&&&&&&&& Console.WriteLine("加热器已经开启", heatTime);&&&&&&& }&&&&&& private int heatTime;&&&&&& private void Heat()&&&&&& {&&&&&&&&&& while (true)&&&&&&&&&& {&&&&&&&&&&&&&& Console.WriteLine("加热还需{0}秒", heatTime);&&&&&&&&&&&&&&& if (heatTime == 0)&&&&&&&&&&&&&& {&&&&&&&&&&&&&&&&&& RasieBoiledEvent();&&&&&&&&&&&&&&&&&&&&return;&&&&&&&&&&&&&& }&&&&&&&&&&&&&& heatTime--;&&&&&&&&&&&&&& Thread.Sleep(1000);&&&&&&&&&&& }&&&&&& }&& }
OK,简单了,下面是main函数:
class Program
static void Main(string[] args)
var test = new Heater();
test.OnBoiled += TestOnB
test.OnBoiled += TestOnB
test.Begin();
Console.ReadKey();
static void TestOnBoiled(object sender, EventArgs e)
Console.WriteLine("Hello事件被调用");
123456789101112131415
class Program&&&&{&&&&&&&&static void Main(string[] args)&&&&&&&&{&&&&&&&&&&&&var test = new Heater();&&&&&&&&&&&&test.OnBoiled += TestOnBoiled;&&&&&&&&&&&&test.OnBoiled += TestOnBoiled;&&&&&&&&&&&&test.Begin();&&&&&&&&&&&&Console.ReadKey();&&&&&&&&}&&&&&&&&static void TestOnBoiled(object sender, EventArgs e)&&&&&&&&{&&&&&&&&&&&&Console.WriteLine("Hello事件被调用");&&&&&&&&}&&&&}
我们有意将事件挂载了两次,看看执行效果:
很明显,如果多次挂载同一事件处理函数,函数将会执行多次。
这就是第一个问题的答案。
接下来,我们将上文中main函数中红色代码替换成如下蛋疼的代码:
test.OnBoiled += TestOnB
test.OnBoiled -= TestOnB
test.OnBoiled -= TestOnB
接下来,我们将上文中main函数中红色代码替换成如下蛋疼的代码:test.OnBoiled += TestOnBoiled;test.OnBoiled -= TestOnBoiled;test.OnBoiled -= TestOnBoiled;
在实际开发中,这种情况是很普遍的,谁都有可能取消订阅多次,结果如何呢?
在执行过程中,删除两次事件没有报错,但当触发事件时,由于事件订阅列表为空,所以,第二个问题的答案:
多次删除同一事件是不会报错的,即使事件只被订阅了一次。若出现订阅三次,取消订阅两次时,依旧执行一次。
这个事情是好理解的,事件列表,实际上就是List,最简单的增删问题。
三. 有了匿名函数后?
自从学习匿名函数后,笔者就特别喜欢用它,除非代码量特别长,否则十行之内的事件订阅,我都会用匿名函数。可是事情变得有意思了,写了匿名函数后,几乎没人记得取消订阅,那么,发生了什么事情呢?
和上次一样,我们将前面红色代码改成下面的样子:
test.OnBoiled += (s, e) =& Console.WriteLine("加热完成事件被调用");&br&test.OnBoiled -= (s, e) =& Console.WriteLine("加热完成事件被调用");&br&test.Bein();
test.OnBoiled += (s, e) =& Console.WriteLine("加热完成事件被调用");&br&test.OnBoiled -= (s, e) =& Console.WriteLine("加热完成事件被调用");&br&test.Bein();
Resharper直接给我画了灰线,如下图:
我估计情况不太乐观,执行之后:
果然!加热完成事件还是被调用了,也就是说,看着形式完全一致的两个匿名函数,编译器生成的方法签名是不一致的,根本就是两个不同的函数。因此,匿名函数完全没法取消订阅! 这是第三个问题的答案。
事件不能被取消订阅!这下可惨了,我真的要取消怎么办?没办法,只能乖乖的写完整的事件函数。匿名方法虽好,千万别用过头。
但是,真正麻烦的问题来了,一个复杂的动态系统中,一定随时会有大量的对象生成和销毁,你也一定会给它订阅一些事件,当你用匿名函数后,这些函数是不是就像死神一样,一直掐着你的脖子? 如果事件处理函数涉及重要操作,比如给对方付款,执行多次你是不是就要哭死了?
四. 垃圾回收和事件
垃圾回收机制搀和进来后,故事变的更有意思了。
我“殷切”的希望,垃圾回收器会帮我解决第三节最后一段谈到的问题,帮我收拾掉那些函数,那真实的情况呢?我们做个试验:
同样的,替换掉红色部分:
test.OnBoiled += (s, e) =& Console.WriteLine("加热完成事件被调用");
test=new Heater();
GC.Collect();
//强制垃圾回收实际上可有可无
test.Bein();
test.OnBoiled += (s, e) =& Console.WriteLine("加热完成事件被调用");test=new Heater();GC.Collect();&&//强制垃圾回收实际上可有可无test.Bein();
下面是执行结果:
哈,起码在我更新了对象引用,new了新对象之后,原来的匿名事件确实没有了。看来编译器还是够意思的。
可是,多数实际开发情况中,我们很少直接new一个对象覆盖掉原来的引用。而是重新new了一个对象出来。这种情况的代码如下
test.OnBoiled += (s, e) =& Console.WriteLine("加热完成事件被调用");
var heaters = new List() { test, test };
heaters.Clear();
test.Begin();
GC.Collect();
test.OnBoiled += (s, e) =& Console.WriteLine("加热完成事件被调用");&&&&&&&&&&&&var heaters = new List() { test, test };&&&&&&&&&&&&heaters.Clear();&&&&&&&&&&&&test.Begin();&&&&&&&&&&&&test = null;&&&&&&&&&&&&GC.Collect();
执行结果如下图:
这种情况下,test即使被赋值为null,事件还是会乖乖执行,因为是匿名函数,你也没法取消订阅,而GC强制收集也没用! 这就是我们真实场景中最可怕的事情,你认为它已经消失了,可是它还挂在事件上!
其实这里有个破绽:Heater类里开了线程,我即使赋值为null,线程肯定还没有被销毁,事件确实可能会执行,时间所限,我没有尝试在写一个类测试不开线程的情况,有兴趣的读者可以帮忙试一试。
而且,经过我查阅资料,当你的对象订阅了外部的事件,而又没有取消订阅,那么该对象是不会被GC回收的!这会造成很恐怖的问题,产生了几千万个对象没法被回收。可是,匿名函数让我怎么么取消订阅?!
所以我们得到了结论,除非确实是一般场景,比如界面开发的window,生成了一直存在,或者在应用程序关闭时回收,否则少用匿名函数吧!记得取消事件订阅!否则会是非常麻烦的事情!
五.高潮: 多线程和事件
多线程本来就是程序员头疼的问题,笔者在多线程知识上只是入门,没开发过高并发系统,倒是经常用并行库加速算法执行。 让我们看看多线程和事件两个最难搞的东西纠缠在一起时是个什么样子。
一种常见的场景,是事件处理很耗时,比如执行长时间的IO操作,或者进行了复杂的数学计算,我们不想影响主线程,那么你想当然的会通过多线程的方法解决。
创建对象的线程,一般是主线程(或者UI线程),那么,怎么让事件处理函数在另外一个线程执行呢? 你真的保证处理函数在另外一个线程中执行了?异步调用?好办法,不过我们此处不说这个。
//////////////////**************///////////////////////////
修正:经过了重新的测试,发现我的测试用例写的有问题,为了让Heater类自己触发事件,我在内部写了一个新线程,导致测试不准确。
结论应该是: 不论是不是在多线程环境下,事件处理函数一定在触发事件位置所在的线程中,和事件订阅者的创建线程,订阅事件时所在的线程无关。。。。。。我第五节的内容,有多半都是错的。。。。
因此,若是触发事件所在线程是主线程的话,基本上只能用我提出的第二种做法,通过事件内部使用线程池来执行了。感谢
/////////////////*************/////////////////////
1. 新建线程方法:
初学者会这么做:
test.OnBoiled += (s, e) =&
var newThread = new Thread(
new ThreadStart(
Thread.Sleep(2000); //模拟长时间操作
Console.WriteLine("总算把热好的水加到了暖瓶里");
newThread.Start();
test.Begin();
123456789101112
test.OnBoiled += (s, e) =&&&&&&&&&&&&&&&&&{&&&&&&&&&&&&&&&&&&&&var newThread = new Thread(&&&&&&&&&&&&&&&&&&&&&&&&new ThreadStart(&&&&&&&&&&&&&&&&&&&&&&&&&&&&() =&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&{&&&&&&&&&&&&&&&&&&&&&&&&&&&& Thread.Sleep(2000); //模拟长时间操作&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&Console.WriteLine("总算把热好的水加到了暖瓶里");&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&}));&&&&&&&&&&&&&&&&&&&&newThread.Start();&&&&&&&&&&&&&&&&};&&&&&&&&&& &&&&&&&&&&&&test.Begin();
我的手指还是选择了匿名函数,用起来真爽,这种情况下,显然事件处理函数所在线程和主线程不一样。
可是,稍微有点基础的人就知道,当事件被频繁触发时,线程就会被频繁生成,线程同样是非常昂贵的系统资源,更何况,线程的启动时间是不确定的,可能会耽误大事。这不是个好方案。
采用.NET 4.0的线程池试试看,代码如下:
var mainThread = Thread.CurrentT
test.OnBoiled += (s, e) =&
ThreadPool.QueueUserWorkItem((d) =&
Thread.Sleep(2000); //模拟长时间操作
Console.WriteLine("总算把热好的水加到了暖瓶里");
if (Thread.CurrentThread != mainThread)
Console.WriteLine("两者执行的是不同的线程");
Console.WriteLine("两者执行的是相同的线程");
test.Begin();
123456789101112131415161718
var mainThread = Thread.CurrentThread;&&&&&&&&&&&&test.OnBoiled += (s, e) =&&&&&&&&&&&&&&&&&{&&&&&&&&&&&&&&&&&&&&ThreadPool.QueueUserWorkItem((d) =&&&&&&&&&&&&&&&&&&&&&&&&&{&&&&&&&&&&&&&&&&&&&&&&&&&&&&Thread.Sleep(2000); //模拟长时间操作&&&&&&&&&&&&&&&&&&&&&&&&&&&&Console.WriteLine("总算把热好的水加到了暖瓶里");&&&&&&&&&&&&&&&&&&&&&&&&&&&&if (Thread.CurrentThread != mainThread)&&&&&&&&&&&&&&&&&&&&&&&&&&&&{&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&Console.WriteLine("两者执行的是不同的线程");&&&&&&&&&&&&&&&&&&&&&&&&&&&&}&&&&&&&&&&&&&&&&&&&&&&&&&&&&else&&&&&&&&&&&&&&&&&&&&&&&&&&&&{&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&Console.WriteLine("两者执行的是相同的线程");&&&&&&&&&&&&&&&&&&&&&&&&&&&&}&&&&&&&&&&&&&&&&&&&&&&&&});&&&&&&&&&&&&&&&&};&&&&&&&&&&&&test.Begin();
我们通过缓存主线程,并比较处理函数中的线程,得到结果如下:
确实,采用线程池时,会是两个是不一样的线程,线程池由于内部做了管理,因此可以有效的利用线程,避免疯狂新开线程造成的严重的性能问题。
可是,我觉得还是麻烦,尤其是有多种事件时,挨个写线程池还是太麻烦了。那么,我们是不是有两种方案?
一种是将构造函数写在一个新线程中,另外一种是将事件订阅函数写在新线程中,两者会发生怎样的情况呢?
3. 对象的构造函数处在新线程时:
如下测试代码:
var mainThread = Thread.CurrentT
var autoResetEvent = new AutoResetEvent(false);
//通过信号机制保证对象首先被创建
ThreadPool.QueueUserWorkItem((d) =&
test=new Heater();
autoResetEvent.Set();
autoResetEvent.WaitOne();
test.OnBoiled += (s, e) =& Console.WriteLine(Thread.CurrentThread != mainThread ? "两者执行的是不同的线程" : "两者执行的是相同的线程");
test.Begin();
12345678910
var mainThread = Thread.CurrentThread;&&&&&&&&&&&&var autoResetEvent = new AutoResetEvent(false);&&//通过信号机制保证对象首先被创建&&&&&&&&&&&&ThreadPool.QueueUserWorkItem((d) =&&&&&&&&&&&&&&&&&{&&&&&&&&&&&&&&&&&&&&test=new Heater();&&&&&&&&&&&&&&&&&&&&autoResetEvent.Set();&&&&&&&&&&&&&&&&});&&&&&&&&&&&&autoResetEvent.WaitOne();&&&&&&&&&&&&test.OnBoiled += (s, e) =& Console.WriteLine(Thread.CurrentThread != mainThread ? "两者执行的是不同的线程" : "两者执行的是相同的线程");&&&&&&&&&&&&test.Begin();
代码值得一提的是,为了保证对象被首先创建,采用了信号机制实现线程同步,当创建后,主线程才会往下执行,否则会抛出空引用的异常.
结果如下:
可见: 主线程称为Main, 若对象构造函数在B线程执行,事件不在主线程中执行。那是不是在B线程中执行呢?暂时还不知道。
4. 对象的事件订阅函数处在新线程时:
在另外一个线程里创建对象是更麻烦的,你要解决线程同步问题,恶心不,哈哈。
那么,若订阅事件的代码在线程B时,情况是怎样的呢?
var mainThread = Thread.CurrentT
ThreadPool.QueueUserWorkItem((d) =&
var bThread = Thread.CurrentT
test.OnBoiled += (s, e) =&
if(Thread.CurrentThread == mainThread )
Console.WriteLine("事件在主线程中执行");
else if (bThread==Thread.CurrentThread)
Console.WriteLine("事件在订阅事件的线程B中执行");
Console.WriteLine("事件在第三个线程中执行");
test.Begin();
1234567891011121314151617181920
var mainThread = Thread.CurrentThread; &&&&&&&&&&&&ThreadPool.QueueUserWorkItem((d) =&&&&&&&&&&&&&&&&&{&&&&&&&&&&&&&&&&&&&&var bThread = Thread.CurrentThread;&&&&&&&&&&&&&&&&&&&&test.OnBoiled += (s, e) =&&&&&&&&&&&&&&&&&&&&&&&&&{&&&&&&&&&&&&&&&&&&&&&&&&&&&&if(Thread.CurrentThread == mainThread )&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&Console.WriteLine("事件在主线程中执行");&&&&&&&&&&&&&&&&&&&&&&&&&&&&else if (bThread==Thread.CurrentThread)&&&&&&&&&&&&&&&&&&&&&&&&&&&&{&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&Console.WriteLine("事件在订阅事件的线程B中执行");&&&&&&&&&&&&&&&&&&&&&&&&&&&&}&&&&&&&&&&&&&&&&&&&&&&&&&&&&else&&&&&&&&&&&&&&&&&&&&&&&&&&&&{&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&Console.WriteLine("事件在第三个线程中执行");&&&&&&&&&&&&&&&&&&&&&&&&&&&&}&&&&&&&&&&&&&&&&&&&&&&&&};&&&&&&&&&&&&&&&&});&&&&&&&&&&&&&test.Begin();
说实话,我看到这个场景的时候大吃一惊,居然执行事件的代码不在主线程,不在订阅事件的线程,而在另外一个第三者线程!这可能就是线程池的无敌之处吧,它连事件订阅函数都给托管了!真是碉堡了!!
不过,管它是什么线程里执行,反正我主线程是不会被堵塞了,哈哈.
本来想今天把最后一个问题都解决的,可是时间实在太晚,而且文章已经够长了。不妨最后一个问题,“在复杂软件环境下,如何理性正确的使用委托和事件”放在第二部分吧。有些问题我也没搞清,在做实验的情况下,才逐渐接近结论。 写完这篇文章,我深有收获。
其实,按照惯例,应该把IL代码好好搞出来给大家看才算是“专业”的选择,不过我确实不懂IL,就不拿出来丢人了,高手们请自行脑补。
本文介绍了C#的委托和事件的订阅和取消订阅,并在匿名函数和多线程两个环境下讨论了一些问题。如果你觉得这篇文章对你有帮助,请点一下推荐,若有任何问题,欢迎留言讨论,共同学习。
测试代码见,请将不同Region的代码解开注释进行测试。
可能感兴趣的话题
test.OnBoiled += (s, e) =& Console.WriteLine("加热完成事件被调用");
var heaters = new List() { test, test };
heaters.Clear();
test.Begin();//事件订阅在这里已经被通知执行了,和后面的语句没关系
GC.Collect();
关于伯乐在线博客
在这个信息爆炸的时代,人们已然被大量、快速并且简短的信息所包围。然而,我们相信:过多“快餐”式的阅读只会令人“虚胖”,缺乏实质的内涵。伯乐在线内容团队正试图以我们微薄的力量,把优秀的原创文章和译文分享给读者,为“快餐”添加一些“营养”元素。
新浪微博:
推荐微信号
(加好友请注明来意)
&#8211; 好的话题、有启发的回复、值得信赖的圈子
&#8211; 分享和发现有价值的内容与观点
&#8211; 为IT单身男女服务的征婚传播平台
&#8211; 优秀的工具资源导航
&#8211; 翻译传播优秀的外文文章
&#8211; 国内外的精选文章
&#8211; UI,网页,交互和用户体验
&#8211; 专注iOS技术分享
&#8211; 专注Android技术分享
&#8211; JavaScript, HTML5, CSS
&#8211; 专注Java技术分享
&#8211; 专注Python技术分享
& 2016 伯乐在线.Net开发(1)
委托与事件是C#中的重要概念,这两个概念既有联系又有区别,容易混淆,下面就对C#中的这两个概念进行一下比较。
声明委托的语法和声明函数非常类&#20284;,不过需要使用delegate关键字,并没有函数体。例如,以下代码声明了一个委托:
public delegate double ProcessDelegate(double param1, double param2);
这个委托声明了一个名为ProcessDelegate的,返回&#20540;为double,接收两个double类型的参数的函数原型,而且这个委托是public的,具有全局访问权限。委托是一个类型,就像int、double一样,都是类型名,要声明一个委托型变量,应该再写:
这样就声明了一个ProcessDelegate型的变量process。这个变量有点类&#20284;与函数指针,能指向任何一个符合ProcessDelegate原型的函数。例如:
double Multiply(double param1,double param2)&{&&&&&&&& return param1 * param2;&}
process = new ProcessDelegate(Multiply);
Multiply是一个符合ProcessDelegate委托原型的函数,process这个委托变量可以“指向”它,利用了一个稍显古怪的语法:process = new ProcessDelegate(Multiply);这个语法是委托所特有的,它采用了new关键字,然后接委托类型,然后把要指向的函数名放在一个括号里。可以利用一个简单的语法来实现:
这两个语法是等价的,都把process这个委托变量“指向”了Multiply。
既然process已经“指向”了Multiply,任何使用Multiply的地方都能用process代替,例如process(1,2)这个语句和Multiply(1,2)是等价的。
事件的实现需要利用委托。我们拿一个自定义事件为例进行说明。
public class Connection
&&&&&&& public delegate void MessageHandler(string messageText);
&&&&&&& public event MessageHandler MessageA
以上语句声明了一个事件MessageArrived,声明事件之前我们先声明了一个名为MessageHandler的委托。可以看出,与委托有明显的区别,事件不是一个类型,而是类的一个成员,是属于类的,和字段、属性、方法一样,都是类的一部分。声明MessageArrived事件需要使用关键字event,并在前面加上委托类型的名称,如果不加关键字event就和上文所述的声明委托变量一样了。事件前面的委托类型说明,处理事件的函数必须符合委托所指定的原型形式。
MessageArrived(&Hello Mun!&);语句使程序“抛出”一个事件,就像“抛出”一个异常一样。然而,事件并不需要外界使用try...catch语句捕获,而是该类的实例捕获。在另一个程序中,通过代码:
Connection myConnection = new Connection();
myConnection.MessageArrived &#43;= new MessageHandler(DisplayMessage);
public void DisplayMessage(string message){&&&&&&Console.WriteLine(&Message arrived:{0}&, message);}
进行捕获。其中关键在于:myConnection.MessageArrived &#43;= new MessageHandler(DisplayMessage);就像委托“指向”了一个函数一样,为事件添加了一段事件处理程序。每当事件被触发时,就会调用DisplayMessage函数进行事件处理。
以上就是委托与事件的基本联系与区别。
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:900次
排名:千里之外
(1)(1)(1)(2)(3)

我要回帖

更多关于 事件委托 的文章

 

随机推荐