介绍.NET中的委派(一)
----微软 .NET平台系列文章之四
回调函数
回调函数的确是至今为止最有用的编程机制之一。C运行时的qsort函数利用回调函数对数组元素进行排序。在Windows中,回调函数更是窗口过程,钩子过程,异步过程调用,以及目前Microsoft .NET框架所必需的,在整个回调过程中自始至终地使用回调方法。人们可以注册回调方法以获得加载/卸载通知,未处理异常通知,数据库/窗口状态修改通知,文件系统修改通知,菜单项选择,完成的异步操作通知,过滤一组条目等等。
在C/C++中,一个函数的地址就是内存地址。这个地址不会附带任何其它赋加信息,如函数的参数个数,参数类型,函数的返回值类型以及这个函数的调用规范。简言之,C/C++回调函数不是类型安全的。
在.NET框架中,回调函数所受到的重用与它在Windows非受控编程中一样。不同的是在.NET框架中提供了一种叫委派(delegates)的类型安全机制。我们先来研究一下委派的声明。下面的代码展示了如何声明,创建和使用委派:
//
using System;
using System.WinForms;// 在beta2版本中为:System.Windows.Forms
using System.IO;
class Set {
private Object[] items;
public Set(Int32 numItems) {
items = new Object[numItems];
for (Int32 i = 0; i < numItems; i++)
items[i] = i;
}
// 定义 Feedback,类型为delegate
// (注意: 这个类型在Set类中是嵌套的)
public delegate void Feedback(
Object value, Int32 item, Int32 numItems);
public void ProcessItems(Feedback feedback) {
for (Int32 item = 0; item < items.Length; item++) {
if (feedback != null) {
// 一旦指定了回调,便调用它们
feedback(items[item], item + 1, items.Length);
}
}
}
}
class App {
[STAThreadAttribute]
static void Main() {
StaticCallbacks();
InstanceCallbacks();
}
static void StaticCallbacks() {
// 创建一个Set对象,其中有五个项目
Set setOfItems = new Set(5);
// 处理项目,feedback=null
setOfItems.ProcessItems(null);
Console.WriteLine();
// 处理项目,feedback=Console
setOfItems.ProcessItems(new Set.Feedback(App.FeedbackToConsole));
Console.WriteLine();
// 处理项目,feedback =MsgBox
setOfItems.ProcessItems(new Set.Feedback(App.FeedbackToMsgBox));
Console.WriteLine();
// 处理项目,feedback = console AND MsgBox
Set.Feedback fb = null;
fb += new Set.Feedback(App.FeedbackToConsole);
fb += new Set.Feedback(App.FeedbackToMsgBox);
setOfItems.ProcessItems(fb);
Console.WriteLine();
}
static void FeedbackToConsole(
Object value, Int32 item, Int32 numItems) {
Console.WriteLine("Processing item {0} of {1}: {2}.",
item, numItems, value);
}
static void FeedbackToMsgBox(
Object value, Int32 item, Int32 numItems) {
MessageBox.Show(String.Format("Processing item {0} of {1}: {2}.",
item, numItems, value));
}
static void InstanceCallbacks() {
//创建一个Set对象,其中有五个元素
Set setOfItems = new Set(5);
// 处理项目,feedback=File
App appobj = new App();
setOfItems.ProcessItems(new Set.Feedback(appobj.FeedbackToFile));
Console.WriteLine();
}
void FeedbackToFile(
Object value, Int32 item, Int32 numItems) {
StreamWriter sw = new StreamWriter("Status", true);
sw.WriteLine("Processing item {0} of {1}: {2}.",
item, numItems, value);
sw.Close();
}
}
//
注意代码最上面的Set类。假设这个类包含一组将被单独处理的项目。当你创建Set对象时,将它要操纵的项目数传递给它的构造函数。然后构造函数再创建对象(Objects)数组并初始化每一个整型值。
另外,Set类定义了一个公共的委派,这个委派指出某个回调函数的签名。在这个例子中,委派Feedback 确定了一个带三个参数的方法(第一个参数为Object,第二和第三个参数都是Int32类型)并且返回void。在某种意义上,委派很像C/C++中表示某个函数地址的类型定义(typedef)。
此外,Set类定义了一个公共方法:ProcessItems。这个方法有一个参数feedback——一个对委派Feedback 对象的引用。ProcessItems迭代遍历所有的数组元素,并且针对每一个元素调用回调方法(由feedback变量指定哪一个会调方法),这个回调方法被传递,从而以不同的方式处理回调方法所传递的项目值,项目数量以及数组中的元素数目。可以看出回调方法能以它选择的任何方式处理每一个项目。
使用委派调用静态方法
StaticCallbacks方法示范了用各种不同方式的回调委派。这个方法首先构造一个Set对象,告诉它对象创建有五个对象元素的数组。然后调用ProcessItems,在第一个调用中,它的feedback参数为null。ProcessItems呈现一个方法,这种方法为每一个Set操纵的项目实现某种动作。在第一个例子中,因为feedback参数是null,在处理每一个项目时不调用任何回调方法。
第二个例子中创建了一个新的Set.FeedBack委派对象,这个委派对象是一个方法包装器,允许方法的调用是经由这个包装器间接调用。对于类型FeedBack的构造器来说,方法的名字(App.FeedBackConsole)被作为构造器的参数传递;这就表示方法被包装。然后,从new操作符返回的引用被传到ProcessItems。现在,当执行ProcessItems时,它会调用App类型的FeedbackToConsole方法处理集合中的每一个项目。FeedbackToConsole简单地将一个串输出到控制台,表示哪个项目被处理了以及这个项目的值是什么。
第三个例子与第二个例子基本相同。唯一的差别是Feedback委派对象包装的是另一个方法:App.FeedbackToMsgBox。这个方法建立一个串,用这个串表示哪个项目被处理以及这个项目的值是什么。然后将这个串显示在一个信息框中。
第四个例子也是静态调用的最后一个例子示范了如何将委派链接在一起形成一个链。在这个例子中,首先创键一个Feedback委派对象的引用变量fb,并将它初始化为null。这个变量指向委派链表的头。Null值表示链表中没有节点。然后,构造Feedback委派对象,由这个对象包装对App FeedbackToConsole方法的调用。C#中,+=操作符被用于将对象添加到fb引用的链表中。Fb此时指向链表的头。
最后,构造另一个Feedback委派对象,由这个对象包装对App FeedbackToMsgBox方法的调用。C#中的+=操作符又一次被用于将对象添加到fb引用的链表中,并且fb被新的链表的头更新。现在,当执行ProcessItems时,传递给它的是Feedback委派链表的头指针。在ProcessItems内部,调用回调方法的代码行实际上终止调用所有的在链表中由委派对象包装的回调方法。也就是说,对于被迭代的每一个项目,都会调用FeedbackToConsole,接着马上调用FeedbackToMsgBox。在后续文章中我将详细讨论委派链的处理机制。
有一点很重要,那就是在这个例子中每件事情都是类型安全的,例如,当构造Feedback委派对象时,编译器保证App的FeedbackToConsole和FeedbackToMsgBox方法都具备确切的原型,像由Feedback委派定义的一样。既两个方法必须有三个参数(Object,Int32和Int32),并且两个方法必须有相同的返回类型(void)。如果方法原型不匹配,则编译器将发出下面的出错信息:“error CS0123:The signature of method 'App.FeedbackToMsgBox()' does not match this delegate
type。”——意思是App.FeedbackToMsgBox()方法的签名与委派的类型不匹配。
调用实例方法
前面我们讨论了如何使用委派调用静态方法。但是委派还能被用于调用特定对象的实例方法。在调用实例方法时,委派需要知道这个它要用方法操作的对象的实例。
为了理解实例方法的回调机制,让我们回头看看前面代码中的InstanceCallbacks方法。这段代码与静态方法的情形极其相似。注意在Set对象被创建之后,App对象被创建。这个App对象仅仅是创建而已,处于示例目的没有其它内容。当新的Feedback委派对象被创建的时候,它的构造齐备传到appobj.FeedbackToFile。这将导致这个委派包装对FeedbackToFile方法的引用,FeedbackToFile是个实例方法(非静态)。当这个实例方法被调用时,由appobj引用的对象被操作(作为隐藏传递参数)。FeedbackToFile方法的作用有点像FeedbackToConsole 和 FeedbackToMsgBox,不同的是它打开一个文件并将处理的项目串添加到文件尾。
揭开委派的神秘面纱
从表面上看,委派好像很容易使用:用C#委派关键字定义,用类似new操作符的方式构造它们的实例, 用类似方法调用的语法调用回调方法(不同的是不使用方法名,而是使用指代委派对象的变量)。
然而,委派的实际运行机制要比前述例子中所描述的过程要复杂的多。编译器和公共语言运行时(CLR)在幕后所做的许多处理隐藏了这些复杂性,在这一部分中,我们将集中精力来讨论编译器和CLR是如何协同工作实现委派机制的。这些知识将极大地丰富你对委派的理解并且这些知识将告诉你如何有效地使用它们。我们还将涉及到一些在编程中能用到的委派的附加特性。
我们还是从下面这行代码开始:
public delegate void Feedback(
Object value, Int32 item, Int32 numItems);
当编译器看到之一行代码时,它会产生一个完整的类定义,这个定义的代码会像下面这个样子:
//
public class Feedback : System.MulticastDelegate {
// 构造器
public Feedback(Object target, Int32 methodPtr);
// 方法与源代码描述的原型相同
public void virtual Invoke(
Object value, Int32 item, Int32 numItems);
// 方法允许被异步回调,后继文章将讨论这些方法
public virtual IAsyncResult BeginInvoke(
Object value, Int32 item, Int32 numItems,
AsyncCallback callback, Object object);
public virtual void EndInvoke(IAsyncResult result);
}
//
事实上,通过使用ILDasm.exe程序检查结果模块(如图三),你能发现编译器确实自动产生了这个类。
图三 检查编译器产生的类
在这个例子中,编译器已经定义了一个叫Feedback的从System.MulticastDelegate类型派生的类,它是在框架类库(Framework Class Library)中定义的。要知道,所有委派类型都是从MulticastDelegate派生出来的。在这个例子中,Feedback类是公共(public)类型的,因为在源代码中它的类型被定义为public。如果用私有(private)或者受保护的(protected)类型定义,则由编译器产生的Feedback类也将是私有或者受保护的类型。你应该注意到委派类可能会在某个类中定义(如例子中Feedback就是在Set类中定义的);委派也可能在被定义为全局型。从本质上说,可以将委派看成是类,可以在定义类的任何地方定义委派。
因为所有的委派都派生于MulticastDelegate,它们继承了MulticastDelegate的域,属性和方法。在所有这些成员中,你要特别注意三个私有(private)域:
用于委派类型的私有域:
域
类型
描述
_target
System.Object
指回调函数被调用时应该操作的对象。用于实例方法回调
_methodPtr
System.Int32
内部整型,CLR用它来标示被回调的方法
_prev
System.MulticastDelegate
指另一个委派对象,通常为null
所有的委派都有代两个参数的构造器:一个参数是对象引用,一个是指代回调方法的整型。但是,如果你检查源代码,就会发现明白诸如App.FeedbackToConsole 或 appobj.FeedbackToFile的传递使用值进行的。你的敏感会告诉你这个代码不能编译!
然而,编译器知道某个委派被创建,同时编译器解析源代码以决定引用哪个对象和方法。对象引用被传递为目标参数,并且用某个特定的Int32值(从某个MethodDef或MethodRef元数据符号获得)标示的方法被传递为methodPtr参数。对于静态方法,null被传递为目标参数。在构造器内部,这两个参数被存储在它们对应的私有(private)域中。
另外,构造器将这个域置为null。这个域被用来创建一个MulticastDelegate对象链表。现在我们暂时忽略_prev域,在后续文章中将会详细讨论有关它的内容。
每一个委派对象实际上就是一个方法包装器,当方法被调用时,受作用的对象被操作。MulticastDelegate类定义两个只读公共实例属性:Target和Method。给定一个委派对象引用,你就可以查询到它的这些属性。如果方法被回调,Target属性返回一个对将要操作的对象的引用。如果方法是静态的,则Target返回null。Method属性返回标示回调方法的System.Reflection.MethodInfo对象。
你可以用几种方式使用这些信息。一种方式是检查是否某个委派对象引用特定类型的实例方法:
//
Boolean DelegateRefersToInstanceMethodOfType(
MulticastDelegate d, Type type) {
return((d.Target != null) && d.Target.GetType == type);
}
//
你还应该编写代码检查是否回调方法由专门的名字(如FeedbackToMsgBox):
//
Boolean DelegateRefersToMethodOfName(
MulticastDelegate d, String methodName) {
return(d.Method.Name == methodName);
}
//
现在你知道了如何构造委派对象,下面让我们来谈谈回调方法是如何被调用的。为方便起见,我们还是使用
Set类中的ProcessItems:
//
public void ProcessItems(Feedback feedback) {
for (Int32 item = 1; item <= items.Length; item++) {
if (feedback != null) {
// 如果指定任何回调,则调用它们
feedback(items[item], item, items.Length);
}
}
}
//
注释行下面的那一行代码就是调用回调方法。仔细看看代码,它调用feedback函数并传递三个参数。但是feedback是不存在的。再一次指出,编译器知道feedback是个引用某个委派对象的变量,并且编译器会产生实际的代码来调用委派对象的Invoke方法。换句话说,编译器看到下面这行代码后:
feedback(items[item], item, items.Length);
编译器产生的结果与下面这行源代码产生的结果一样:
feedback.Invoke(items[item], item, items.Length);
事实上,通过使用ILDasm.exe程序检查ProcessItems代码结果(如图五),你能发现这一点。
图五 分解后的 Set类的 ProcessItems
图五显示了用于Set类型中ProcessItems方法的微软中介语言。其中红色的箭头指向的指令调用Set.Feedback的Invoke方法。如果你修改源代码来显式调用Invoke方法,C#编译器报错,出错信息为:“error CS1533: Invoke cannot be called directly on a delegate”——意思是Invoke不能针对某个委派被直接调用。C#不允许你显式调用Invoke(但是,但别的编译器可以)。
你会想起当编译器定义Feedback类的时候,它也定义了Invoke方法。当Invoke被调用时,它使用私有的_target和_methodPtr域来为特定对象调用希望的方法。注意Invoke方法的签名与委派的签名要完全匹配。也就是说,Feedback委派带三个参数并返回void,那么Invoke方法也必须带三个相同的参数并返回void。