《CLR via C#》 委托

在非委托C/C++中,非成员函数的地址只是一个内存地址。这个地址不携带任何额外信息。它们很轻量但是不是类型安全的。

委托使用方式

 1// 声明一个委托类型,它的实例引用一个方法
 2// 该方法获取一个int参数,返回void
 3public delegate void DelFunc(int value);
 4
 5public class DelClass {
 6    public static void Func1(int value) {
 7        Console.WriteLine("Func1 " + value);
 8    }
 9
10    public static void Func2(int value) {
11        Console.WriteLine("Func2 " + value);
12    }
13
14    public void Func3(int value) {
15        Console.WriteLine("Func3 " + value);
16    }
17}
18
19DelClass dc = new DelClass();
20DelFunc df1 = new DelFunc(DelClass.Func1);  // 引用静态函数
21DelFunc df2 = new DelFunc(DelClass.Func2);  // 引用静态函数
22DelFunc df3 = new DelFunc(dc.Func3);        // 引用实例函数
23
24// 利用Combine方法向委托添加多个引用函数
25DelFunc dfs = null;
26dfs = (DelFunc) Delegate.Combine(dfs, df1);
27dfs = (DelFunc) Delegate.Combine(dfs, df3);
28
29// 利用Remove方法移除委托中的方法
30dfs = (DelFunc) Delegate.Remove(dfs, new DelFunc(dc.Func3));
31
32// 简化的添加与删除
33dfs += df1;
34dfs -= df1;

提示

委托对象其实是方法的包装器(wrapper),使方法能够通过包装器来间接回调。

注意

在一个类型中通过委托来调用另一个类型的私有成员,只要委托对象是由具有足够安全性/可访问性的代码创建的,便没有问题。 将方法绑定到委托时,允许参数或返回值中的引用类型使用协变性和逆变性。

1delegate Object Callback(FileStream s);
2// 上方的委托可以用以下的方法原型来绑定
3String Func1(Stream s);
4// 但不能用以下的方法原型来绑定,因为int是值类型
5int Func2(Stream s);

揭秘委托的内幕

1public delegate void DelFunc(int value);

上述代码定义了一个委托,而这段委托定义会让编译器定义一个完整的类:

1public class DelFunc : System.MulticastDelegate {
2    // 构造器
3    public DelFunc(Object @object, IntPtr method);
4    public virtual void Invoke(int value);
5    // 以下方法实现对回调方法的异步回调
6    public virtual IAsyncResult BeginInvoke(int value, AsyncCallback callback, Object @object);
7    public virtual void EndInvoke(IAsyncResult result);
8}

所有的委托类型都派生自System.MulticastDelegateSystem.MulticastDelegate又派生自System.DelegateSystem.Delegate又派生自System.Object

MulticastDelegate中的三个重要的非公共字段

字段 类型 说明
_target System.Object 当委托对象包装一个静态方法时,这个字段为null。当委托对象包装一个实例方法时,这个字段引用的是回调方法要操作的对象。换言之,这个字段指出要传给实例方法的隐式参数this的值
_methodPtr System.IntPtr 用来标识要回调的方法
_invocationList System.Object 该字段通常为null。构造委托链时它引用一个委托数组

委托的构造器获取两个参数:一个对象引用,一个引用了回调方法的整数。看起啦应该不会通过编译才对。而编译器知道要构造的时委托,所以会分析代码来确定引用的是哪个对象和方法。对象的引用被传递给构造器的object参数,标识了方法的特殊IntPtr值(从MethodDef或MemberRef元数据获取)被传递给构造器的method参数。静态方法会为object传递null

 1// 引用静态方法
 2// _target = null
 3// _methodPtr = Func1
 4// _invocationList = null
 5DelFunc fstatic = new DelFunc(Func1);
 6
 7DelClass dc = new DelClass();
 8// 引用实例方法
 9// _target = DelClass对象dc
10// _methodPtr = Func3
11// _invocationList = null
12DelFunc fInstance = new DelFunc(dc.Func3);

而调用委托的回调方法其实就是调用其委托类中的Invoke方法

1public void Callback(DelFunc df, int value) {
2    if (df != null) {
3        df(value);
4        // 编译器编译后会转换成以下代码
5        df.Invoke(value);
6    }
7}

委托链

添加委托

1DelFunc dfs = null;
2dfs = (DelFunc) Delegate.Combine(dfs, df1);
3dfs = (DelFunc) Delegate.Combine(dfs, df2);

第一次调用Delegate.Combine方法时将nulldf1合并,返回df1

第二次合并发现dfs已经引用了一个委托,所以会构造一个新的委托对象。新委托对象对它的私有字段_target_methodPtr进行初始化。_invocationList字段被初始化为引用一个委托对像数组。数组的第一个元素被初始化为引用包装df1方法的委托。第二个元素为引用了df2方法的委托。最后dfs被设为引用新委托的对象。

委托链

删除委托

1dfs = (DelFunc) Delegate.Remove(dfs, new DelFunc(dc.Func3));

Remove方法被调用时,扫描第一个参数dfs的委托对象内部的委托数组(从末尾向索引0扫描)。查找其_target_methodPtr与数组中匹配的项。如果找到匹配且删除后还剩一个数据项就返回那个数据项如果找到匹配且有多个数据项,就新建一个委托对象并初始化_invocationList引用除删除项其它的所有数据项。并返回对这个新建委托对象的引用

Invoke方法调用委托链

如果委托为

1public delegate int DelFunc(int value);

则其Invoke方法的伪代码为

 1public int Invoke(int value) {
 2    int result;
 3    Delegate[] delegateSet = _invocationList as Delegate[]'
 4    if (delegateSet != null) {
 5        // 这个委托数组指定了应该调用的委托
 6        foreach (DelFunc d in delegateSet) {
 7            result = d(value); // 调用每个委托
 8        }
 9    } else { // 不是委托链的情况
10        // 该委托标识了要回调的单个方法,在指定的目标对象上调用这个回调方法
11        // 以下的代码接近实际情况,不过用C#无法表达出来
12        result = _methodPtr.Invoke(_target, value);
13    }
14    // 数组中每个委托被调用时,其返回值被保存到result中。循环过后只会保存最后一个委托的结果,之前的都会被抛弃。
15    return result;
16}

对委托链的更多控制

对委托链的调用会有很多局限性,比如返回值只有最后一个调用才能返回。如果其中一个委托中有异常就会阻塞后续所有委托的执行。所以MulticastDelegate类提供了一个实例方法GetInvocationList,用于显式调用链中的每一个委托。

1public abstract class MulticastDelegate : Delegate {
2    public sealed override Delegate[] GetInvocationList();
3}

GetInvocationList方法在内部初始化一个数组,让它每个元素都引用链中的一个委托,然后返回对该数组的引用。如果_invocationList字段为null,返回的数组就只有一个元素,该元素引用链中唯一的委托,既委托本身。

委托的简化

1. 不需要构造委托对象

1public static void Call(DelFunc func) {
2    func?.Invoke();
3}
4
5// Call(new DelFunc(DelClass.Func1));
6// 可简写为
7Call(DelClass.Func1);

2. 不需要定义回调方法(lambda表达式)

1public class A {
2    public static void Callback() {
3        ThreadPool.QueueUserWorkItem(obj => Console.WriteLine(obj), 5);
4    }
5}

编译器看到lambda表达式后会在类中自动定义一个新的私有方法。这个方法为匿名函数。方法名带有‘<’和‘>’符号来区别与其他函数。CLR允许带‘<’或‘>’符号的方法名。而且C#编译器会向其应用特性System.Runtim.CompilerServices.CompilerGeneratedAttribute,来表明方法是由编译器生成的。

注意

lambda表达式没有办法定制特性,也不能引用任何方法修饰符(如unsafe)。编译器生成的匿名函数总是私有方法。根据是否访问了任何实例成员来决定方法的静态和非静态。如果没有访问实例成员会生成静态方法,因为静态方法效率更高。

3. 局部变量不需要手动包装到类中即可传递给方法

C#编译器会在类中生成一个辅助类来存储当前类中的局部变量,和匿名方法。然后调用匿名方法的时候就可以调用到局部变量了,但是会导致局部变量的声明周期变长,并且无法被GC回收。

委托和反射

为了解决调用委托时无法事先知道回调方法原型的情况。利用System.Delegate.MethodInfo提供的CreateDelegate方法来创建委托。