分类: C/C++
2008-04-01 09:22:57
在上一篇文章中我介绍了微软.NET框架中的回调方法:委派(delegates)。解释了在声明一个委派后编译器是如何产生一个从System.MulticastDelegate派生的类,以及这个类是如何创建两个私有指针域(_target 和 _methodPtr),并用它们来指明回调方法应该操纵哪个对象。我还在上一篇文章中引入了第三个指针域——_prev,这个指针被用来维护委派链的链表。在本篇文章中,我将把焦点集中在_prev指针域,讨论如何管理和使用委派链的链表。 |
回顾委派的一些历史——System.Delegate 和 System.MulticastDelegate |
.NET框架类库中定义了System.MulticastDelegate类。在上一篇文章中我们对这个类进行了讨论。但是,你应该注意到MulticastDelegate实际上是从System.Delegate(也在.NET框架类库中定义)类派生而来的,System.Delegate本身派生于System.Object。 |
当最初设计.NET框架的时候,微软的工程师们感觉到需要提供两种不同种类的委派:即single-cast 和 multicast。MulticastDelegate类型用来表示能被链接在一起的对象,而Delegate类型用来表示不能被链接在一起的对象。System.Delegate类型被设计为基本类型,这个类实现了需要回调某个打包方法的所有功能。MulticastDelegate类从Delegate类中派生并赋予了创建MulticastDelegate对象链表(或者说链)的能力。 |
当编译源代码时,编译器将检查委派签名并适当选择两个以上的类用于编译产生委派类型的基类。说起来你会感到稀奇,具有void返回值的带签名的方法会从System.Delegate派生,而具有非void返回值的方法则从System.MulticastDelegate派生。这样给人的感觉就是你只能从链表链中最后那个方法获得返回值。 |
在.NET框架beta版测试期间,采用两种不同的基本类型对开发人员所产生的误导越来越明显。此外,用这种方法来设计委派有太多的局限性。例如,许多方法的返回值在好多情况下都可以被忽略掉。由于这些方法会有非void类型返回值,他们不会从MulticastDelegate类中派生,防止了它们被组合进某个链表。 |
为了减少引起的混乱,微软工程师意图将Delegate和MulticastDelegate类整合为一个单独的类,这个类允许任何委派对象参与到某个链表链中。所有编译器会产生从这个单独的类中派生的委派类。这个变化将降低复杂性,并且将会使.NET框架团队,公共语言运行时团队(CLR),编译器团队以及在这个领域使用委派的第三方开发者从中受益。 |
遗憾的是这个整合Delegate和MulticastDelegate的想法在.NET框架开发周期中来得稍迟了一些,况且微软当时最关心的问题是潜在的Bugs,要求通过测试重现这些Bugs以确定最需要修改的部分。所以在.NET框架的Beta 1中没有整合这些类。你当然希望在将来的.NET框架版本中将它们整合到一个单独的类中。 |
虽然微软选择了延期整合.NET框架类库中的这两个类,但他们还是修改了所有的编译器,以便这些编译器现在总是从MulticastDelegate类派生来产生委派类型。所以在我的上一篇文章中,我说过这么一句话:“所有的委派类型都是从MulticastDelegate派生的。”因为编译器的改动,从而所有委派类型的实例都可以被组合到某个链表链中而与它们的返回值无关。 |
那么为什么要理解这些东西呢?当你开始越来越多地接触到委派时,你肯定会在.NET框架SDK文档中碰到Delegate和MulticastDelegate类型。我的意图是要你理解这两个类之间的关系。此外,即便是所有你创建的委派类型都以MulticastDelegate作为基类,偶尔你还是会碰到用Delegate所定义的方法来代替MulticastDelegate方法处理自己的委派类型的情况。 |
例如,Delegate类的静态方法是Combine和Remove。(我将会在以后解释这些方法地用途。)这两个方法的签名表示它们带Delegate参数。因为你的委派类型是从MulticastDelegate派生的,而MulticastDelegate又是从Delegate派生,你的委派类型实例可能被传到Combine和Remove方法。 |
比较委派的等同性 |
Delegate基类覆盖了 Object 的虚拟 Equals 方法。MulticastDelegate类型继承Delegate的Equals实现。Delegate的Equals实现将两个委派对象进行比较,检查它们的_target和_methodPtr指针域是否引用相同的对象和方法。如果这两个指针域相匹配,则Equals返回true,否则Equals返回false。如下面的代码所示: |
// // 构造两个委派对象,它们引用相同的目标/方法 // Feedback fb1 = new Feedback(FeedbackToConsole); Feedback fb2 = new Feedback(FeedbackToConsole); // 虽然 fb1 和 fb2 引用两个内部不同的对象,但都引用相同的回调目标/方法 // Console.WriteLine(fb1.Equals(fb2)); // 显示Displays "True" // |
此外,Delegate 和 MulticastDelegate都提供相等(==)和不等(!=)操作的重载。因此,你可以使用这些操作符而不用调用Equals方法。下列代码与上面所列出来的相同 |
// // 构造两个委派对象,它们引用相同的目标/方法 // Feedback fb1 = new Feedback(FeedbackToConsole); Feedback fb2 = new Feedback(FeedbackToConsole); // 虽然 fb1 和 fb2 引用两个内部不同的对象,但都引用相同的回调目标/方法 // Console.WriteLine(fb1==fb2); // 显示Displays "True" // |
在处理委派链的时候,理解如何比较两个委派的相等性是很重要的,下面我们就来讨论委派链。 |
委派链(Delegate Chains) |
委派本身不但用处很大,而且它还支持委派链处理,从而使得它的使用更加举足轻重。在上一篇文章中,我曾谈到每一个MulticastDelegate对象都具备一个私有的_prev域。这个域存储对另一个MulticastDelegate对象的引用。也就是说,每一个MulticastDelegate对象类型(或者从MulticastDelegate派生的类型)都具备对另一个MulticastDelegate派生对象的引用。这个域允许将委派对象加到某个链表中。 |
Delegate类定义了三个静态方法,可以用它们来处理委派对象的链表链: |
// class System.Delegate { // 这个函数联合由head & tail表示的链,head 被返回 // (注意: head 是最后被调用的委派) public static Delegate Combine(Delegate tail, Delegate head); // 创建有委派数组表示的某个链 // (注意: 入口 0 是 head 并将是最后被调用的委派) public static Delegate Combine(Delegate[] delegateArray); // 从链中删除某个委派匹配值的目标/方法。 // 韩回新的 head 并将是最后被调用的委派 public static Delegate Remove(Delegate source, Delegate value); } // |
当你构造一个新的委派对象时,这个对象的_prev域被置为null,表示在链表中没有其它对象。为了将两个委派组合(combine)到某个链表中,可以调用两个Delegate的静态方法之一来完成: |
Feedback fb1 = new Feedback(FeedbackToConsole); Feedback fb2 = new Feedback(FeedbackToMsgBox); Feedback fbChain = (Feedback) Delegate.Combine(fb1, fb2); // 图一的左边显示了在前面的代码执行后链的状态 App appobj = new App(); Feedback fb3 = new Feedback(appobj.FeedbackToStream); fbChain = (Feedback) Delegate.Combine(fbChain, fb3); |
图一 委派链(Delegate Chains) |
图一展示了所有代码执行后的链。你会注意到 Delegate 类型提供了Combine方法的另一个带Delegate引用数组为参数的版本。用这个版本的Combine可以向下面一样重写前面所列出的代码: |
Feedback[] fbArray = new Feedback[3]; fbArray[0] = new Feedback(FeedbackToConsole); fbArray[1] = new Feedback(FeedbackToMsgBox); App appobj = new App(); fbArray[2] = new Feedback(appobj.FeedbackToStream); Feedback fbChain = Delegate.Combine(fbArray); |
当某个委派被调用的时候,编译器产生一个对委派类型Invoke方法的调用。(这在前面的文章中已讨论过)为了刷新内存,在前面的文章中曾声明了一个Feedback委派代码如下: |
public delegate void Feedback( Object value, Int32 item, Int32 numItems); |
这样将导致编译器产生一个包含Invoke方法的Feedback类,如下面的伪码: |
class Feedback : MulticastDelegate { public void virtual Invoke( Object value, Int32 item, Int32 numItems) { // 如果链表中还有委派,则应该首先调用它们 if (_prev != null) _prev.Invoke(value, item, numItems); // 针对特定的目标对象调用回调方法 _target.methodPtr(value, item, numItems); } } |
正像你所看到,调用某个委派对象导致其前一个委派被首先调用。当前一个委派返回时,其返回值被丢弃。在调用其前一个委派之后,这个委派再调用它打包的回调目标/方法。下面的代码示范了这个过程: |
Feedback fb1 = new Feedback(FeedbackToConsole); Feedback fb2 = new Feedback(FeedbackToMsgBox); Feedback fbChain = (Feedback) Delegate.Combine(fb1, fb2); // 这里,fbChain 引用一个调用FeedbackToMsgBox的委派,而这个委派又引用另一个调用 FeedbackToConsole // 的委派,此委派引用null. // 现在我们调用头(head)委派,它在内部调用其前面一个委派... if (fbChain != null) fbChain(null, 0, 10); // 注意: 上面这行代码中, 必须检查 fbChain 是否为空,在调用它之前进行此类检查是个很好的习惯。 |
以上是我说明的一个返回值为 void 的委派 Feedback。如果我像下面这样定义委派的话: |
public delegate Int32 Feedback( Object value, Int32 item, Int32 numItems); |
class Feedback : MulticastDelegate { public Int32 virtual Invoke( Object value, Int32 item, Int32 numItems) { // 只要链中有应被首先调用的委派就调用它们 if (_prev != null) _prev.Invoke(value, item, numItems); // 调用特定目标对象的回调方法 return _target.methodPtr(value, item, numItems); } } |
当委派链的头被调用时,它调用链中的前一个委派。注意这里前一个委派的返回置被丢弃。你的应用程序代码将只能从链头的委派中接收返回值(即调用的最后一个回调用法)。 |
委派对象一旦被创立,它就被认为是不能改变的,也就是说委派对象总是将它们的_prev域置为null,并且不会再变。当把某个委派对象组合到链中时,Combine在内部构造一个新的委派对象,它于源对象有着相同的_target 和 _methodPtr指针域。_prev域被置成链中旧的头。新委派对象的地址从Combine中返回。请看下面的代码: |
//将委派对象组合到链中 Feedback fb = new Feedback(FeedbackToConsole); Feedback fbChain = (Feedback) Delegate.Combine(fb, fb); // fbChain 引用有两个委派对象的链。其中一个对象与fb所指的对象相同。 // 另一个对象由Combine构造,这个对象的_prev域引用fb,并且Combine // 返回对新对象的引用。 // fb 和 fbChain 引用完全相同的对象吗?False Console.WriteLine((Object) fb == (Object) fbChain); // fb 和 fbChain 引用相同的回调目标/方法吗?True Console.WriteLine(fb.Equals(fbChain)); |
现在你知道了如何建立委派链,那如何从链中删除一个委派呢?想从某个链表中删除一个委派,必须调用Delegate类型中的静态Remove方法,参见如下代码: |
// 调用 Delegate.Remote Feedback fb1 = new Feedback(FeedbackToConsole); Feedback fb2 = new Feedback(FeedbackToMsgBox); Feedback fbChain = (Feedback) Delegate.Combine(fb1, fb2); // fbChain 引用两个委派的一个链 // 调用这个链: 两个方法被调用 if (fbChain != null) fbChain(null, 0, 10); fbChain = (Feedback) Delegate.Remove(fbChain, new Feedback(FeedbackToMsgBox)); // fbChain 引用一个委派的链 // 调用这个链: 一个方法被调用 if (fbChain != null) fbChain(null, 0, 10); fbChain = (Feedback) Delegate.Remove(fbChain, new Feedback(FeedbackToConsole)); // fbChain 引用零个委派的链(fbChain is null) // 调用这个链: 零个方法被调用 if (fbChain != null) fbChain(null, 0, 10); // 线在你该明白了为什么我总要判断 fbChain 是否为null! |
这段代码首先通过构造两个委派对象来建立一个链,然后调用Combine方法将它们组合进一个链表中。然后调用Remove方法。Remove的第一个参数是委派对象链的头,第二个参数是要从链中删除的委派对象。这种处理方法很奇怪,为了将委派对象从链中删除,必须先建立新的委派对象。为了明白其中的原因,我们有必要对此进行一些额外解释。 |
在Remove的调用中,我构造了一个新的委派对象。这个委派对象对它自己的_target和_methodPtr域进行初始化,并将_prev置成null。Remove方法扫描链(fbChain所指的链),检查链中有没有委派对象与新创建的委派对象相等。要记住,由 Delegate 类实现的并经过重载的Equals 方法只比较_target和_methodPtr域并忽略_prev域。 |
如果找到一个匹配的值,Remove 方法则通过固定前一个委派对象的_prev域从链中删除匹配的委派对象。如果没有找到匹配的值,Remove 方法则什么也不做(不丢出异常)并返回与它第一个参数相同的值。 |
每次对Remove的调用只从链中删除一个对象,参见如下代码: |
//删除委派 Feedback fb = new Feedback(FeedbackToConsole); Feedback fbChain = (Feedback) Delegate.Combine(fb, fb); // fbChain 为两个委派的链 // 调用这个链: FeedbackToConsole 被调用两次 if (fbChain != null) fbChain(...); // 从链中删除其中的一个回调 fbChain = (Feedback) Delegate.Remove(fbChain, fb); // 调用这个链: FeedbackToConsole 被调用一次 if (fbChain != null) fbChain(...); // 从链中删除其中的一个回调 fbChain = (Feedback) Delegate.Remove(fbChain, fb); // 调用这个链: 0 个方法被调用 if (fbChain != null) fbChain(...); //C# 对委派链的支持 |
为了让C#开发人员易于开发,C#编译器为委派类型的实例自动地提供 += 和 -= 操作的重载。这两个操作符分别调用Delegate.Combine 和 Delegate.Remove。使用这两个操作符简化了委派链的建立。下列的C#代码演示了如何使用C#操作符简化在一个链中对委派对象进行Combine和Remove操作。 |
//用 C# 对委派对象进行 组合(Combining)和 删除(Removing)操作 Feedback fb = new Feedback(FeedbackToConsole); App appobj = new App(); fb += new Feedback(appobj.FeedbackToStream); // 调用链: FeedbackToStream 和 FeedbackToConsole 被调用 if (fb != null) fb(...); // 从链中删除一个回调 fb -= new Feedback(FeedbackToConsole); // 调用链: FeedbackToStream 被调用 if (fb != null) fb(...); // 从链中删除最后一个回调 fb -= new Feedback(appobj.FeedbackToStream); // 调用链: 0 个方法被调用 if (fb != null) fb(...); // |
编译器在内部翻译所有在委派对象上使用的 += 操作符,使之调用 Delegate 的 Combine 方法。同样,对于在委派对象上使用的 -= 操作符,编译器在内部翻译,使之调用 Delegate 的 Remove 方法。事实上,你可以建立(build)我在这里列出的代码,用ILDasm.exe程序看看它的中间语言是什么样子。这样可以确认C#编译器到底做了一些什么事情,你会从中中间语言中不难看出,编译器分别用 Delegate 类型中静态的 Combine 和 Remove方法代替了 += 和 -= 操作符。 |
调用委派链 |
前面讲述了如何建立一个委派对象的链表链,以及如何调用(Invoke)那个链中的所有对象。因为委派类型的 Invoke 方法包含了对前一个委派进行(如果有的话)调用的代码,所以链表链中所有的项目都被调用。这种方法确实时非常简单明了。这种算法对于大多数遇到的情况都适合,但是它也有许多限制。 |
例如,回调方法的返回值全都被忽略掉了,只留下最后一个。用这种简单的算法没有办法获得所有回调方法运行后的返回值。除此之外,这个算法还有一些局限性。例如,一旦对某个委派的调用丢出异常或长时间阻塞的话会发生什么情况呢?因为这个算法是连续地调用链中每个委派,如果对其中某个委派对象出了问题就会影响链中其它的委派获得调用。很显然,这个算法并不是一个健壮的算法。 |
为了解决这个问题,MulticastDelegate类提供了一个实例方法:GetInvocationList,你可以用这个方法显式地调用链中每一个委派,而所使用的算法可以是任意的: |
//public class MulticastDelegate { // 创建一个委派数组;其中每一项都是链中项目的克隆。 // (注意: 入口 0 是链尾,通常它被首先调用) public virtual Delegate[] GetInvocationList(); } // |
GetInvocationList 方法操作某个委派链的引用并返回一个引用委派对象的数组。GetInvocationList 遍历指定的链并克隆链中每个对象,将这些克隆对象添加到数组中。每个克隆都将其自己的_prev域置为null,这样每个对象被隔离,保证不会与其它的对象链搞混。 |
这样一来我们就很容易编写算法显式地调用数组中的每个对象。下面的代码展示了一种这样的算法。 |
// GetInvocationList 演示 using System; using System.Text; // 定义一个电灯组件 class Light { // 这个方法返回电灯的状态 public String SwitchPosition() { return "The light is off"; } } // 定义一个风扇组件 class Fan { // 这个方法返回风扇的状态 public String Speed() { throw new Exception("The fan broke due to overheating"); } } // 定义一个话筒组件 class Speaker { // 这个方法返回话筒的状态 public String Volume() { return "The volume is loud"; } } class App { // 定义委派允许查询某个组件的状态 delegate String GetStatus(); static void Main() { // 定义一个空的委派链 GetStatus getStatus = null; // 构造3个组件并将它们的状态方法添加到委派链中 getStatus += new GetStatus(new Light().SwitchPosition); getStatus += new GetStatus(new Fan().Speed); getStatus += new GetStatus(new Speaker().Volume); // 显示反映3个组件当时状态的报告 Console.WriteLine(GetComponentStatusReport(getStatus)); } // 查询组件状态的方法及返回某种状态报告 static String GetComponentStatusReport(GetStatus status) { // 如果链是空,什么也不做 if (status == null) return null; // 用它来建立状态报告 StringBuilder report = new StringBuilder(); // 获得一个数阻,其元素是链中的委派 Delegate[] arrayOfDelegates = status.GetInvocationList(); // 遍历数组中的每一个委派 foreach (GetStatus getStatus in arrayOfDelegates) { try { // 获得某个组件的状态串并将它追加到报告中 report.Append(getStatus() + "\r\n\r\n"); } catch (Exception e) { // 在报告中产生本组件出错入口 Object component = getStatus.Target; report.Append("Failed to get status from " + ((component == null) ? "" : component.GetType() + ".") + getStatus.Method.Name + "\r\n Error: " + e.Message + "\r\n\r\n"); } } // 将获得的状态返回到调用者 return report.ToString(); } } |