委托判等
FCL中Delegate重写了Object的Equals虚方法,MulticastDelegate又重写了Delegate的Equals实现。MulticastDelegate重写的Equals方法在比较两个委托对象时会首先看它们的_target和_methodPtr字段是否都指向同样的对象和方法。如果这两个字段不匹配,那么返回false。如果这两个字段都匹配,那么再看两个委托对象是否为委托链表的头部(即_prec字段不为null)。如果两个委托对象的_prev字段指示的链表有相同的长度,且两个链表上对应委托对象的_target和_methodPtr字段也都互相匹配,那么Equals返回true,否则返回false。
接上节中示例:
TestDelegate td3 = new TestDelegate(tc.Length);
TestDelegate td4 = new TestDelegate(tc.Length);
Console.WriteLine(td3 == td4); // 输出true
td4 += new TestDelegate(tc.Display);
Console.WriteLine(td3 == td4); // 输出false
当我们操作委托链时,理解如何比较两个委托对象是否相等非常重要。
委托链
每个MulticastDelegate对象都有一个私有字段_prev。该字段指向另一个MulticastDelegate对象的引用。这使得多个委托对象可以组合成一个链表。
Delegate类中定义了3个静态方法帮助我们操作委托链表。
class System.Delegate
{
// 组合head和tail所表示的链表,并返回head。
// head将是最后一个被调用的委托对象
public static Delegate Combine(Delegate tail, Delegate head);
// 创建一个由委托数组表示的委托链表。
// 索引为0的元素将为链表尾部,且有第一个被调用的委托对象
public static Delegate Combine(Delegate[] delegateArray);
// 从链表中移除一个和value的回调目标/回调方法相匹配的委托
// 新的链表头会被返回,且将是最后一个被调用的委托对象
public static Delegate Remove(Delegate source, Delegate value);
}
当我们构造一个新的委托对象,其_prev字段会被设为null。要将两个委托对象组合成一个链表,我们可以调用Delegate两个静态Combine方法中的任何一个。
// 以上节代码为例
TestDelegate td1 = new TestDelegate(Sum);
TestDelegate td2 = new TestDelegate(Sum);
TestDelegate tdChain = (TestDelegate)Delegate.Combine(td1, td2);
TestDelegate[] tdArray = new TestDelegate[ 2 ];
TestDelegate[ 0 ] = new TestDelegate(Sum);
TestDelegate[ 1 ] = new TestDelegate(Sum);
tdChain = (TestDelegate)Delegate.Combine(tdArray);
当一个委托对象被调用时,编译器会产生对该委托类型的Invoke方法的调用。
继续以上节的代码为例,描述Invoke方法内部的源码(伪码描述):
public void virtual Invoke( int [] array)
{
// 如果链表上包含有任何应该被首先调用的委托,那么将对它们进行递归调用
if (_prev != null )
_prev.Invoke(array);
// 在指定目标对象上调用回调方法
_target.methodPtr(array);
}
这样通过递归调用,链表上的所有回调方法都被执行一遍。
注意:如果委托方法带有返回值,那么唯有链表头部的那个委托调用(即最后一次调用的回调方法)的返回值被返回,其他的委托调用返回值都被丢弃。
委托对象一旦被构造,它们就被认为是恒定不变的。也就是说,委托对象的_prev字段总被设置为null,且不会改变。当我们调用Combine将一个委托对象组合到一个委托链中时,Combine方法在内部会构造一个新的委托对象,新委托对象和源委托有着同样的_target和_methodPtr字段,但是其_prev字段被设置指向原先的委托链的头部。最后Combine方法返回新委托对象的地址。
委托对象的移除:
// 以上节代码为例
TestDelegate td1 = new TestDelegate(Sum);
TestDelegate td2 = new TestDelegate(Sum);
TestDelegate tdChain = (TestDelegate)Delegate.Combine(td1, td2);
tdChain = (TestDelegate)Delegate.Remove(tdChain, new TestDelegate(Sum));
构造一个新的委托对象的目的就是为了将其从委托链中移除,是不是很奇怪?
在调用Remove时,我们构造了一个新的委托对象。该委托对象_target和_methodPtr字段都得到了适当的初始化,_prev字段被初始化为null。Remove方法首先扫描tdChain引用的委托链,检查其上是否有和新建的委托对象相等的委托对象。如果找到一个匹配,Remove方法便从委托链上移除找到的委托对象,这可以通过修正委托链上找到的那个委托对象前一个委托对象的_prev字段来达到。Remove方法最终返回新委托链的头部。如果找不到,Remove方法将不执行任何操作,亦不会抛出异常,而直接返回传递给它的第一个参数。
注意:如果在委托链上找到了要移除的委托对象或委托链,那么每一次调用Remove方法都只移除从链表头开始的第一个这样的委托对象或委托链。
为减轻开发人员的编程工作,C#编译器自动为委托类型实例提供了+=和-=操作符重载,这两个操作符分别调用Delegate.Combine和Delegate.Remove方法。