委托 实际很简单(中)
在上文中,我介绍了如何定义委托并进行简单的调用。对于最后留下的那个问题,答案当然是否定的。但是为什么?我们用delegate关键字定义的是什么?委托类型啊(注意,是委托类型而不是委托类型的实例)。跟使用class关键字定义的其它类一样,委托类型只不过是一种相对特殊的类罢了,你什么时候见过可以在一个函数体内声明一个class了。所以,搞清楚这一点很重要,你用delegate声明的是一个类,你需要用这个类去实例化一个对象,才可以使用,这恰恰是初学者最容易混淆的地方。本文,我们来继续说说委托的其它用法。
委托用途之二 异步调用
由于后面的内容涉及到了线程的知识,如果你还不了解什么是线程,请先上网去补补课再继续啊。
什么是异步调用?我们写的大多数代码,线程在调用一个方法或函数的时候,都会等到被调用的方法或函数执行完毕,然后再继续执行后面的代码。我们这样调用方法或函数的过程,就是同步调用。而异步调用,就是相对于同步调用的过程来说的。线程在异步调用一个方法或函数的时候,不会等待方法或函数执行完毕,就会继续执行被调用的方法或函数的后面的代码。那么当前线程是如何做到调用了一个方法或函数后,不用等它结束,就继续往下执行了呢?实际上,被异步调用的方法或函数是在一个新的线程上被执行的。也就是说,所谓的异步调用,就是由当前的线程(A)创建了一个新的线程(B),然后由这个新线程(B)来执行被调用的方法或函数。那么,当前的线程(A)是如何知道被调用的方法或函数已经在新线程(B)中执行完了呢?一般情况下,我们在开始一个异步调用的时候,都会指定一个回调方法或函数,异步调用结束的时候,新线程(B)会调用回调方法或函数通知线程(A)。
为什么要使用异步调用? 对于一个Windows程序来说,它的主线程也就是UI线程,主要负责用户界面的显示并且响应用户的操作。如果你使用UI线程直接处理非常耗时的操作,比如连接到远程的数据库插入大量的数据,或者从服务器上面下载一个较大的文件。这个时候,由于你调用的方法长时间无法返回,你的主线程就无法处理用户的请求,比如说取消操作,这样你的程序看上去就停止响应了。看下面的例子程序:
1: private void button1_Click(object sender, EventArgs e)
2: {
3: label1.Text = "开始了";
4: MakeMeStop();
5: label1.Text = "结束了";
6: }
7:
8: private void button2_Click(object sender, EventArgs e)
9: {
10: isCancel = true;
11: }
12:
13: bool isCancel = false;
14:
15: private void MakeMeStop()
16: {
17: for (int i = 0; i < 10; i++)
18: {
19: System.Threading.Thread.Sleep(1000);
20: if (isCancel)
21: {
22: isCancel = false;
23: return;
24: }
25: }
26: }
27:
当你点击button1的时候,你会发现,在MakeMeStop运行结束前,你的程序不会响应你的任何操作,包括你想通过点击button2取消当前操作。这就是由于你调用的MakeMeStop方法会一直占用UI线程,直到它结束。
好,下面我们修改一下这个例子,看看如何通过委托的异步调用,来解决这个问题。
1: public delegate void SampleDelegate();
2: private void button1_Click(object sender, EventArgs e)
3: {
4: label1.Text = "开始了";
5: SampleDelegate dele = new SampleDelegate(MakeMeStop);
6: dele.BeginInvoke(new AsyncCallback(Finish), null);
7: }
8:
9: private void button2_Click(object sender, EventArgs e)
10: {
11: isCancel = true;
12: }
13:
14: bool isCancel = false;
15:
16: private void MakeMeStop()
17: {
18: for (int i = 0; i < 10; i++)
19: {
20: System.Threading.Thread.Sleep(1000);
21: if (isCancel)
22: {
23: isCancel = false;
24: return;
25: }
26: }
27: }
28:
29: private void Finish(IAsyncResult result)
30: {
31: MessageBox.Show("结束了");
32: }
象前一篇文章介绍的那样,首先,我们先定义一个委托类型SampleDelegate,同我们想要调用的MakeMeStop方法的签名一样,SampleDelegate没有任何参数和返回值。然后,我们在buttion1的Click事件中,声明一个SampleDelegate类型的实例dele,让它指向MakeMeStop方法。最后,我们看第6行的调用方法,跟上一章介绍的不同,这里我们调用了委托(dele)的BeginInvoke方法,就是这个方法,为我们创建了一个新的线程去执行MakeMeStop方法。这样我们的UI线程在调用了BeginInvoke方法后会立即返回,以便去响应其它的用户输入。
我们来看一下BeginInvoke方法的参数
象我前面说过的那样,当异步线程结束后,需要调用一个回调函数或方法来通知主线程。这里的第一个参数AsyncCallback就是要你指定一个回调函数,这个参数类型实际上就是一个委托类型,它的签名是什么样的呢?我们来看一下它的定义:
public delegate void AsyncCallback(IAsyncResult ar);
从上面的定义我们知道,这个回调函数需要有一个IAsmyncResult类型的参数,并且没有返回值。所以在上面的程序中,我定义了一个Finish函数作为异步调用结束的回调函数。我们再来看一下回调函数中的参数IAsyncResult,一般情况下,我们在回调函数中会通过这个参数得到异步调用的信息,列出它的成员:
AsyncState:这是一个object类型,这个值对应了BeginInvoke方法的第二个参数,也就是说如果你的执行异步调用的时候,有需要传递的数据给回调函数,可以通过这个对象获得。
CompletedSynchronously:bool类型,这个值表示发起异步调用的线程(就是调用BeginInvoke方法的那个线程)是否已经结束。
IsCompleted:bool类型,这个值表示当前的异步调用是否结束。看到这里是不是有疑问?没有?那我有问题了,回调函数是在异步调用的方法执行结束的时候被调用的,那么这里这个值岂不是总是true?那这个值是有什么用?确实,在回调函数中,这个值用处不大,但不要忘了,对于IAsyncResult这个接口类型来说,并不是只用于回调函数。回头看看BeginInvoke方法的返回值,我们看到,它的返回值也是一个IAsyncResult接口类型。如果你在执行BeginInvoke方法时象下面这样:
1: IAsyncResult result = dele.BeginInvoke(new AsyncCallback(Finish), null);
那么我们就可以在主线程中,使用result的IsCompleted属性来判断异步调用是否结束了。
AsyncWaitHandle:WaitHandle类型。如果你的主线程,在执行了BeginInvoke方法后,在某一时刻需要等待异步调用的方法执行结束,再接着往下执行,那么该如何处理?方法一:做一个循环,判断IsCompleted是否为true,当为true的时候,表示异步调用结束。方法二:就是通过AsyncWaitHandle属性,执行AsyncWaitHandle.WaitOne()方法,这个方法会在异步调用结束后返回。既然要在主线程中等待异步调用的结束,为什么还要异步调用?我现在能给出的答案就是,使用了异步调用,这个等待我可以根据程序的需要,在调用后的任何时候进行,比如我在异步调用了方法A之后,我可以马上调用方法B和方法C,然后由于某些原因,主线程必须等到方法A执行完毕,才能继续执行方法D,这个时候,我的程序就可以控制何时才需要进行等待。(实际上,大多数情况下,当主线程执行完方法C的时候,异步调用的方法A可能已经执行结束,调用AsyncWaitHandle.WaitOne()方法只是为了确保A已经被执行完毕)。
在本文的最后,还是留下一个问题给大家:前面的程序中,如果我把Finish方法中的“MessageBox.Show("结束了");”改成 “label1.Text = "结束了";”会出现什么问题?(对于这类问题如何解决,我会专门写相关的文章进行解释)