什么是委托? -> 初识委托
在很多应用程序中(C,C++),需要对象使用某种回调机制,能够与创建它的实体进行通信,在.NET平台下,通过委托来提供了一种回调函数机制,在.NET平台下,委托确保回调函数是类型安全的(这也正是.NET FreamWork与非托管代码的区别)。本质上来讲,委托是一个类型安全的对象,它指向程序中另一个以后会被调用的方法(或多个方法),就是将方法作为参数来传递.
C#中定义委托类型
在C#中创建一个委托类型时,需要使用关键字 delegate 关键字,类型名可以自定义,但是 委托要指定一个回调方法的签名.
1 //声明一个委托,该委托可以指向任何传入两个Int类型并且方法的返回值为Int. 2 public delegate int Binary(int x,int y); 3 4 //声明一个委托,该委托可以指向任何传入一个String类型并且方法返回值为Void 5 public delegate void DelegateBackCall(string str);
使用委托发送对象状态通知 -> 用委托回调静态方法
先来看一段代码:
1 void Main() 2 { 3 Program.Main(); 4 } 6 public delegate void DelegateBackCall(int value); 7 class Program 8 { 9 public static void Main() 10 { 11 Counter(1,4,null); 12 Counter(1,4,new DelegateBackCall(StaticDelegateToConsole)); 13 } 15 private static void Counter(int x,int y,DelegateBackCall foo) 16 { 17 for(var i = x;i <= y;i++) 18 { 19 if(foo != null) 20 foo(i); 21 } 22 } 23 private static void StaticDelegateToConsole(int num) 24 { 25 Console.WriteLine("Item : " + num); 26 } 28 }
首先 定义了一个名字为DelegateBackCall委托,该委托指定的方法要获取Int类型参数,返回void,在Program类中定义了私有的静态方法Counter,用来统计X到Y之间整数的个数,同时呢,Counter方法还获取一个Foo,Foo是对一个DelegateBackCall委托对象的引用, 在方法体中 我们首先遍历一下,如果Foo不为null,就调用Foo变量所指定的回调函数.那么传入的这个回调函数的是正在处理的那个数据项的值.
然后 在Program的Main函数中,第一次调用Counter时,第三个参数传递的是Null,所在在Counter函数中是不会执行回调函数的.
接着第二次调用Counter函数时,给第三个参数传递一个新构造的DelegateBackCall委托对象(其实在此委托对象是方法的一个包装器, 使方法能通过包装器来间接的进行回调),然后静态方法StaticDelegateToConsole被传给DelegateBackCll委托类型的构造器(StaticDelegateToConsole就是要包装的方法),当Counter执行时,会在遍历每个数据项之后调用静态方法StaticDelegateToConsole,最后输出结果:
1 Result: 2 Item : 1 3 Item : 2 4 Item : 3 5 Item : 4
使用委托发送对象状态通知 -> 用委托调用实例方法
首先还是来看Main()函数中,第三个调用Counter的地方,在此将代码贴出来吧.
1 class Program 2 { 3 public static void Main() 4 { 5 Counter(1,4,null); 6 Counter(1,4,new DelegateBackCall(StaticDelegateToConsole)); 7 8 Counter(1,4,new DelegateBackCall(new Program().InstanceDelegateToMessage)); 9 } 10 private void InstanceDelegateToMessage(int num) 11 { 12 Console.WriteLine("Message : " + num); 13 } 14 }
在第三次调用Counter函数时,第三个参数传递的是Program创建的实例方法,这将委托包装对InstanceDelegateToMessage方法的一个引用,该方法是一个实例方法.同调用静态的一样,当Counter调用Foo回调的时候会调用InstanceDelegateToMessage实例方法,新构造的对象将作为隐式的this参数传给这个实例方法.
最后的输出结果为:
1 Message : 1 2 Message : 2 3 Message : 3 4 Message : 4
通过上面两个例子我们知道委托可以包装对实例方法和静态方法的调用,如果是实例方法,那么委托需要知道方法操作的是哪个对象实例(包装实例是很有用的,对象内部的代码可以访问对象的实例成员,这意味着对象可以维护一些状态,并在回调方法执行期间利用这些状态信心).
委托的协变和逆变
将一个方法绑定到委托时,C#和CLR都允许引用类型的协变和逆变
协变:方法的返回类型是从委托的返回类型派生的一个类型
逆变:方法获取的参数类型是委托参数类型的基类.
例如:
1 delegate object CallBack(FileStream file); 2 3 string SomeMethod(Stream stream);
在上面代码中,SomeMethod的返回类型(string)派生自委托的返回类型(object),这样协变是可以的.
SomeMethod的参数类型(Stream)是委托的参数类型(FileStream)的基类,这样逆变是可以的.
那么如果将String SomeMethod(Stream stream) 改为 Int SomeMethod(Stream stream);
那么C#编辑器会报错.
说明:协变性和逆变性只能用于引用类型,不能用于值类型和Void , 因为值类型的存储结构是变化的,而引用类型的存储结构始终是一个指针.
委托和接口的逆变和协变 -> 泛型类型参数
委托的每个泛型类型参数都可标记为协变量或者逆变量,这样我们即可将泛型委托类型的一个变量转型为同一个委托类型的另一个变量,或者的泛型参数类型不同。
不变量:表示泛型类型参数不能更改。
逆变量:表示泛型类型参数可以从一个基类更改为该类的派生类。在C#中,用in关键字标记逆变量形式的泛型类型参数,逆变量泛型参数只能出现在输入位置.
协变量:表示泛型类型参数可以从一个派生类更改为它的基类,在C#中,用out标记协变量形式的泛型类型参数.协变量只能出现在输出位置.例如:方法返回值类型.
public Delegate TResult Func<in T,out TResult>(T arg);
在上面这行代码中,泛型类型参数 T 用in关键字标记,使它成为了一个逆变量,而TResult用out 关键字标记,这使他成为了一个协变量.
在使用要获取泛型参数和返回值的委托时,尽量使用逆变性和协变性指定in和out关键字.在使用具有泛型类型参数的接口也可将它的类型参数标记为逆变量和协变量,如下代码:
1 public interface IEnumerator<out T> : IEnumerator 2 { 3 Boolean MoveNext(); 4 T Current{get;} 5 } 6 // count方法接受任意类型的参数 7 public int count(IEnumerable<object> coll){} 8 9 //调用count 传递一个IEnumerable<string> 10 int i = count(new[]{"Albin"});
深入委托 -> 委托揭秘
首先来声明一个委托:
1 internal delegate void DelegateBackCall(int val);
通过查看委托反编译:
通过反编译之后看到在DelegateBackCall来中有四个方法:分别为 : 构造器、BeginInvoke、EndInvoke、Invoke 而且还能看到 DelegateBackCall 类是继承 system.MulticaseDelegate(其实所有的委托都是派生自它.本质上来讲:System.MulticaseDelegate是派生自system.Delegate.而后者又派生自system.object),
在此我们还看到 在 第一个构造函数中,有两个参数,一个是对象引用,一个是引用回调方法的一个整数,其实所有委托中都有一个构造器,而且构造器的参数正如刚才所说的(一个对象引用,一个引用回调方法的整数) 在构造器的内部,这两个参数分别保存在_target(当委托对象包装一个静态方法时,这个字段为null,当委托对象包装一个实例方法时,这个字段引用的是回调方法要操作的对象.)和_method(一个内部的整数值,CLR用它来标识要回调的方法) 这两个私有字段呢给 ,此外构造器还将_invocationList(构造一个委托链时,它可以引用一个委托数组,通常为null)字段设为null
每个委托对象实际都是一个包装器,其中包装了一个方法和调用该方法时要操作的一个对象
如下代码:
1 DelegateBackCall DelegateInstance = new DelegateBackCall(Program.InstanceToConsole); 2 3 DelegateBackCall DelegateStatic = new DelegateBackCall(StaticToMessage);
在此 DelegateInstance 和 DelegateStatic 变量引用两个独立的,初始化好的 DelegatebackCall委托对象. 在委托类(Delegate)中定义了两个只读的公共实例属性,Target和Method,当我们给定一个委托对象引用可以查询这些属性.,Target返回的就是我们之前说的_Target字段中的值,指向回调方法要操作的对象,即如果是一个静态的方法那么Target为null,Method属性有一个内部转换机制,可以将私有字段_methodPtr中的值转为为一个MethodInfo对象并返回它.即我们所传递的方法名.
在我们调用委托对象的变量时,实际上编译器所生成的代码时调用的该委托对象的Invoke方法,比如,在Counter函数中,Foo(val) 那么实际上编译器为我们解析为 --> Foo.Invoke(val); Invoke是以一种同步的方式调用委托对象维护每一个方法.意思就是说:调用者必须等待调用完成才能继续执行.Invoke通过_Target和_methodPtr在指定对象上调用包装好的回调方法,Invoke方法的签名与委托的签名是一致的.
用委托回调很多方法 —> 委托链(多播委托)
委托链是由委托对象构成的一个集合.利用委托链,可以调用集合中的委托所代表的任何方法.换句话说就是一个委托对象可以维护一个可调用方法的列表而不是单独一个方法,给一个委托对象添加多个方法时,不用直接分配,在此C#编译器为我们提供了重载 +=,-= 操作符,
那我们还是拿上一代代码来举例,代码如下:
1 DelegateBackCall DelegateInstance = new DelegateBackCall(Program.InstanceToConsole); 2 3 DelegateBackCall DelegateStatic = new DelegateBackCall(StaticToMessage);
这段代码我们可以这样来改装一下:
1 DelegateBackCall DelegateFb = null; 2 3 DelegateFb += new DelegateBackCall(Program.InstanceToConsole); 4 5 DelegateFb += new DelegateBackCall(StaticToMessage);
其实上面这就是委托链(也叫多播委托),
那么我们来反编译一下:
我们在反编译之后发现, +=操作的内部是通过 Delegate类的静态方法Combine将委托添加到链中的.
在此Combine会构造一个新的委托对象,这个新的委托对象对它的私有字段_target和_methodPtr进行初始化, 同时_invocationList字段被初始化为引用一个委托对象数组,数组的第一个元素被初始化为引用包装了Program.InstanceToConsole 方法的委托,数组的第二个元素被初始化为引用了包装了StaticToMessage方法的委托,然后再内部会进行遍历每一个委托进行输出.
同理当我们想移除委托对象集合中一个委托时我们可以通过-= 操作符.如下
DelegateBackCall delegateFb = null; delegateFb -= new DelegateBackCall(new Program().InstanceToConsole); delegateFb -= new DelegateBackCall(StaticToMessage);
那么反编译后同样的道理:
它的内部是通过Remove函数来对委托进行移除的.在Remove方法被调用时,它会遍历所引用的那个委托对象内部维护的委托数组,Remove通过查找其_target和methodPtr字段与第二个参数中的字段匹配的委托,如果找到匹配的委托,并且在删除之后数组中只剩余一个数据项,就返回那个数据项,如果找到,并且数组中还剩余多个数据项,就新建一个委托对象 其中创建并初始化的_invocationList数组将引用原始数组中的所有数据项(被删除的例外),如果删除仅有的一个元素,那么Remove会返回NULL.每次Remove只能删除一个委托.不会删除所有的.
在此说明一个方法:GetInvocationList,这个方法操作一个从 MulticastDelegate派生的对象,返回一个由Delegate引用构成的数组,其中每个引用都指向委托链中的一个委托.在其内部,GetInvocationList构造并初始化一个数组,让它的每个元素都引用链中的一个委托,然后返回对数组的一个引用,如果_invocationList字段为null,返回数组只有一个元素,那么它就是委托实例的本身.
如下代码:
DelegateBackCall delegateFb = null; delegateFb += new DelegateBackCall(new Program().InstanceToConsole); delegateFb += new DelegateBackCall(StaticToMessage); private static void GetComponentReport(DelegateBackcall delegateFo) { if(delegatefo != null ) { Delegate[] arrayDelegates = delegatefo.GetInvocationList(); foreach(DelegateBackCall delegateback in arrayDelegates){ //....省略 } } }
委托定义太多? —> 泛型委托
在.NET Freamework 支持泛型,所以我们可以定义泛型委托,来减少委托声明的个数,这样可以减少系统中类型数目,同时也可以简化编码.
在.NET freamework中为我们提供了17个Action委托,它们从无参数一直到最多16个参数,对于开发来说应该是足够用的了.
除此之外 .NET FreameWork 还提供了17个Function函数,它们允许回调方法返回一个值.
使用获取泛型实参和返回值的委托时,可利用逆变和协变,
委托的简洁写法 -> 1:Lambda表达式、匿名函数的方式来代替定义回调函数。
实际上这些的写法可归纳为C#的语法糖,它为我们程序员提供了一种更简单,更可观方式.
例如:
不需要定义回调函数,直接使用Lambda表达式的形式,创建匿名函数来执行方法体.
Private Static Void CallBackNewDelegateObject() { ThreadPool.QueueUserWorkItem( o => Console.WriteLine(o) ,5); }
传给QueueUserWorkItem方法的第一个实参是一个Lambda表达式,Lambda表达式可在编译器预计会看到一个委托的地方使用,编译器看到这个Lambda表达式之后会在类中自动定义一个新的私有方法,这个方法称为匿名方法,所谓匿名方法,并不是没有名字,它的名字是由编译器自动创建的.在此说明一点,匿名函数是被标记为Static,并且是private 这是因为代码没有访问任何实例成员,不过类中可以引用任何静态字段或静态方法.从而课件匿名函数的性能是比实例方法效率高的,因为它不需要额外的this参数.
2:简化语法 -> 局部变量不需要手动的包装到类中即可传给回调方法
1 public static void UsingLocalVariablesInTheCallBack(int num) 2 { 3 int[] i = new int[num]; 4 AutoResetEvent done = new AutoResetEvent(false); 5 for (int n = 0; n < i.Length; n++) 6 { 7 ThreadPool.QueueUserWorkItem(obj => 8 { 9 int nums = (Int32)obj; 10 i[nums] = nums * nums; 11 if (Interlocked.Decrement(ref num) == 0) 12 { 13 done.Set(); 14 } 15 }, n); 16 } 17 done.WaitOne(); 18 for (int n = 0; n < i.Length; n++) 19 { 20 Console.WriteLine("Index {0},i {1}", n, i[n]); 21 } 22 }
在Lambda表达式的方法体中,如果这个方法体是一个单独的函数,那么我们如何的将变量的值传到方法中呢?那么现在在我们的匿名函数方法体中的代码就需要抽出到一个单独的类中,然后类通过字段赋值每一个值,然后 UsingLocalVariablesInTheCallBack 方法必须构造这个类的一个实例,用方法定义的局部变量的值来初始化这个实例中的字段,然后构造绑定到实例方法的委托对象。