第7条:用委托表示回调
我:“Scott,把院子里的草剪一下,我看会儿书。”
Scott:“爸,我把院子打扫干净了。”
Scott:“爸,我给割草机加油。”
Scott:“爸,割草机怎么不动了?”
我:“我来看看。”
Scott:“爸,弄好了。”
上面这段对话可以说明什么叫作回调。笔者给儿子Scott交代了一项任务,他每完成其中的一部分,就会把任务的进度告诉我,在这个过程中,我依然可以继续做自己的事情。如果发生了重要的情况,或是需要帮忙,那么他可以随时叫我(即便有些情况不太重要,也可以说给我听)。回调就是这样一种由服务端向客户端提供异步反馈的机制,它可能会涉及多线程(multithreading),也有可能只是给同步更新提供入口。C#语言用委托来表示回调。
通过委托,可以定义类型安全的回调。最常用到委托的地方是事件处理,然而除此之外,还有很多地方也可以用。如果想采用比接口更为松散的方式在类之间沟通,那么就应该考虑委托。这种机制可以在运行的时候配置回调目标,并且能够通知给多个客户端。委托是一种对象,其中含有指向方法的引用,这个方法既可以是静态方法,又可以是实例方法。开发者可以在程序运行的时候配置一个或多个客户对象,并与之通信。
由于经常需要使用回调与委托,因此,C#语言提供了一种简便的写法,可以直接用lambda表达式来表示委托。此外,.NET Framework库也用Predicate<T>、Action<>及Func<>定义了很多常见的委托形式。predicate(谓词)是用来判断某条件是否成立的布尔(Boolean)函数,而Func<>则会根据一系列的参数求出某个结果。其实Func<T,bool>与Predicate<T>是同一个意思,只不过编译器会把两者分开对待而已,也就是说,即便两个委托是用同一套参数及返回类型来定义的,也依然要按照两个来算,编译器不允许在它们之间相互转换。Action<>接受任意数量的参数,其返回值的类型是void。
LINQ就是用这些机制构建起来的。List<T>类也有很多方法用到了回调。比方说下面这段代码:
Find()方法定义了Predicate<int>形式的委托,以便检查列表中的每个元素。这是个很简单的回调,Find()方法用它来判断每个元素,并把能够通过测试的元素返回给调用方。编译器会将lambda表达式转换成委托,并以此来表示回调。
TrueForAll()与Find()类似,也要检查列表中的每个元素,只有当所有的元素均满足谓词时,它才会返回true。RemoveAll()可以把符合谓词的元素全都从列表里删掉。
List.ForEach()方法会在列表中的每个元素上面执行指定的操作。编译器会和处理前几条语句时一样,把lambda表达式转换成方法,并创建指向该方法的委托。
.NET Framework里面有很多地方用到了委托与回调。整个LINQ都构建在委托的基础上,而回调则用于处理Windows Presentation Foundation(WPF)及Windows Forms的跨线程封送(cross-thread marshalling)。只要.NET框架需要调用方提供某个方法,它就会使用委托,从而令调用方能以lambda表达式的形式来提供该方法。你自己在设计API时,也应该遵循同样的惯例,使得调用这个API的人能够以lambda表达式的形式指定回调。
由于历史原因,所有的委托都是多播委托(multicast delegate),也就是会把添加到委托中的所有目标函数(target function)都视为一个整体去执行。这就导致有两个问题需要注意:第一,程序在执行这些目标函数的过程中可能发生异常;第二,程序会把最后执行的那个目标函数所返回的结果当成整个委托的结果。
多播委托在执行的时候,会依次调用这些目标函数,而且不捕获异常。因此,只要其中一个目标抛出异常,调用链就会中断,从而导致其余的那些目标函数都得不到调用。
在返回值方面也有类似的问题。开发者可能会定义返回值类型不是void的回调函数。比方说,可以编写这样一段代码,在回调的时候,用CheckWithUser()来判断用户是否要求退出:
如果委托只涉及CheckWithUser()这一项回调,那么上面这段代码是可行的,但如果后面还有其他的回调,那就会出问题:
整个委托的执行结果是多播链(multicast chain)中最后那个函数的返回值,而早前那些函数的返回值则会遭到忽略。因此,CheckWithUser()这个谓词的返回值是不起作用的。
异常与返回值这两个问题可以通过手动执行委托来解决。由于每个委托都会以列表的形式来保存其中的目标函数,因此只要在该列表上面迭代,并把这些目标函数轮流执行一遍就可以了:
笔者所用的这种写法只要发现有一个函数返回false,就不再执行列表中的其他函数了。
总之,如果要在程序运行的时候执行回调,那么最好的办法就是使用委托,因为客户端只需编写简单的代码,即可实现回调。委托的目标可以在运行的时候指定,并且能够指定多个目标。在.NET程序里面,需要回调客户端的地方应该考虑用委托来做。