上一篇文章最后的问题相信大家都已经知道了,没错,如果我们把Finish方法中的“MessageBox.Show("结束了");”改成 “label1.Text = "结束了";”会触发一个控件不能跨线程赋值的异常。对于异步调用,我们现在已经知道,它实际上是用了一个新的线程去执行异步方法,而我们的界面中的所有控件,都是在UI线程上生成了,并且,对于绝大部分的C#的控件来说,它们的实例都是非线程安全的,所以.net平台禁止在一个非UI线程上访问它们(主要是指赋值操作)。所谓的非线程安全,指的就是对于一个控件的实例,在多线程的环境下,.net平台无法保证它的数据可靠性。那么这样的问题我们要如何处理呢?毕竟有时我们需要在回调函数中把一些结果通过界面的控件显示出来。这就涉及到关于多线程操作中,控件的数据的回送操作,我会在单独的文章中讲解。
前面介绍的关于委托的操作,我们都是让一个委托实例指向了一个要调用的方法。那么一个委托的实例同时只能指向一个要调用的方法吗?注意,我说的是同时。答案当然是否定的。也就是说,一个委托的实例,同时可以指向(或关联)多个满足签名的方法,这就是委托的另一个用途,多路广播。代码如下:
1: public delegate void SampleDelegate();
2: private void button1_Click(object sender, EventArgs e)
3: {
4: SampleDelegate dele = new SampleDelegate(m1);
5: dele += new SampleDelegate(m2);
6: dele.Invoke();
7: }
8:
9: private void m1()
10: {
11: }
12:
13: private void m2()
14: {
15: }
注意第5行代码,我们使用了+=操作符,这个操作符将一个新的委托实例添加到当前实例的委托列表中。实际上,我们的委托类型继承自MulticastDelegate,只不过MulticastDelegate是一个特殊的类,它不允许显示的继承(使用class关键字声明它的派生类)。这个委托列表,就来自于MulticastDelegate类。
没错,就像广播一样,当你调用了一个指向多个方法的委托的实例时,委托列表中的方法都会被依次调用。(记住,是依次调用而不是并发的同时调用。)并且,多路广播调用仅支持同步调用,也就是说,你不能使用BeginInvoke进行多路广播的异步调用(不信你就试试,嘿嘿)。
如果你想要将委托列表中的一个方法移除,可以使用下面的代码:
1: dele -= new SampleDelegate(m2);
这样,当你再次调用dele.Invoke的时候,m2就不会再被调用了。
大家请想一想,我们在什么时候会用到多路广播?没错,就是事件。
我相信大家都会使用C#中的控件或类提供的事件,回忆一下,当你给一个控件添加事件的时候,都做了什么?“在控件的事件列表中双击想要添加的事件”,good,这可能是我们最经常干的事情之一了。
那么当你双击之后发生了什么?对,你会看到IDE自动给你生成一个方法,在这个方法中,你要做的就是添上事件处理的代码。
1: private void button1_Click(object sender, EventArgs e)
2: {
3:
4: }
你没有看到的是什么?IDE会在你的Form1.Designer.cs中,为你添加类似下面的代码。
1: this.button1.Click += new System.EventHandler(this.button1_Click);
这段代码熟悉吗?没错,很显然,这个System.EventHandler对象是个委托对象。就像上面介绍的多路广播一样,这段代码将button1_Click方法注册到Click对象的委托列表中了。等等,Click对象不是button1的事件吗?怎么又成了委托对象了?它们有什么联系吗?
让我们看看这段代码中的Click事件是如何声明的?
1: //
2: // Summary:
3: // Occurs when the control is clicked.
4: public event EventHandler Click;
根据Click事件的定义,我们看到它和普通的委托的实例定义有一点区别,就是多了一个event关键字。这么看,事件就是委托的一种特殊的声明方式。那么,event关键字有什么用处?有人说声明事件,废话,当然是声明事件。我的意思是事件跟普通的委托实例到底有什么不同?
我们来做两个实验,首先,我们试试把
1: this.button1.Click += new System.EventHandler(this.button1_Click);
改成
1: this.button1.Click = new System.EventHandler(this.button1_Click);
执行一下,看看发生了什么?哈哈,编译出错了。这个实验告诉我们,使用了event关键字声明的事件不允许赋值操作,也就是说你只能把触发事件要执行的方法注册到事件的委托列表中,或者从委托列表中删除(使用-=)。当你象委托列表添加方法的时候,系统会自动创建一个事件对象的实例。为什么这样要这样做?我自己的想法是,对于一个公共的对象来说,有时候其他人为它的委托列表注册了调用的方法,如果这个时候允许你进行赋值操作的话,会清空别人的注册信息(因为事件对象被你重新分配了),这样对整个系统来说会带来非常大的不确定性,所以事件对象会在它的父对象创建的时候自动创建,后来的人只能维护它的委托列表。
第二个实验,试试把
1: SampleDelegate dele = new SampleDelegate(m1);
改成
1: SampleDelegate dele += new SampleDelegate(m1);
发生了什么?没错,编译又错了。最后请大家想想,这又是为什么呢?
前面的例子帮助大家将一个系统最常用的Click事件分析了一下,经过分析,相信大家一定对c#中的事件是如何工作的有了了解,最后通过一个自定义的事件的例子,来结束我们委托系列的文章。
既然事件是委托的一种特例,那么象使用委托一样,我们要做的第一件事就是声明一个委托类型。
1: public delegate void CookFinishedEventHandler(object sender, CookFinishedEventArgs e);
对于一个委托来说,声明的时候只要符合语法规则,并没有什么其它的限制。但如果这个委托要用于事件的定义,象上面的代码那样,最好遵循下面的命名规则:
1: public class CookFinishedEventArgs : EventArgs
2: {
3: public string FoodName
4: { get; set; }
5: }
这样的话,触发事件的时候,调用者就能通过你自定义的事件参数类获得更多的信息或者通过事件的参数向触发事件的对象传递数据。
当然,在实际中,定义事件的时候你不遵守上面的规则,编译器也不会报错,并且你的程序也会很好的运行(当然前提是你的代码没有逻辑上的错误。)。但我想作为一个好的程序员来说,你的程序就是你的艺术品,遵循了一定规则的代码会让别人看上去更加的赏心悦目吧,除非你是抽象派的~~
声明了事件的委托类型和参数类型后,就是要声明事件了,如下:
1: public event CookFinishedEventHandler CookFinished;
然后在需要触发事件的地方,调用这个委托:
1: public void StartCook()
2: {
3: //do something...
4: if (CookFinished != null)
5: {
6: CookFinishedEventArgs args = new CookFinishedEventArgs();
7: args.FoodName = "红烧肉";
8: CookFinished(this, args);
9: }
10: }
这样,对于一个事件的定义就完成了,关于调用,我想大家一定都没有问题了吧~~
最后附上比较完整的事件定义的代码,关于委托的介绍,就到这里,到这里吧~ 休息休息~~
1: public class Pot
2: {
3: public delegate void CookFinishedEventHandler(object sender, CookFinishedEventArgs e);
4:
5: public class CookFinishedEventArgs : EventArgs
6: {
7: public string FoodName
8: { get; set; }
9: }
10: public event CookFinishedEventHandler CookFinished;
11:
12: public void StartCook()
13: {
14: if (CookFinished != null)
15: {
16: //do something...
17: CookFinishedEventArgs args = new CookFinishedEventArgs();
18: args.FoodName = "红烧肉";
19: CookFinished(this, args);
20: }
21: }
22: }