重温C#基础知识(一)
一、委托
1、委托的含义
委托是一种类型安全的回调函数的机制,是对一类行为的抽象和封装或者说是对具有相同方法签名的一类方法的抽象和封装。委托类型相当于只定义了一个方法的接口,而委托实例就是实现这个接口的一个对象。委托提供了不直接指定一个行为而将行为封装成一个对象,然后就可以在恰当的时间执行该对象封装的行为(像其他对象一样使用)。
回调函数的机制就是在一个方法体内或者代码片段中的某一处并不马上执行已经定义的方法(行为)而是在将来的某一时刻(将来的一个恰当的时间内)执行已经定义的方法。在计划执行已经定义的方法处,就需要一个类型或者类似于标识符的东西(更或者说就是一个容器)对要执行的行为(方法)进行抽象和封装以指出所要执行方法(行为)的签名(也可以说是特点),同时告诉将来执行的时候要执行的是哪一类(什么样方法签名的)行为(方法)。而在将来的要执行已经定义的方法(行为)时,将已经定义的方法(行为)填充进所指定的容器内。
委托就相当于一个容器,一个和它所要装进的行为(方法)具有相同特点或者说形状相同的容器。从这点上看委托其实就像一个只定义一个方法的接口。它并不包含方法的主体而只指出方法的签名,而委托实例就是实现该接口的一个对象。
2、为什么要使用委托?(委托的实质、工作方式和作用)
(1)为什么使用委托?委托的作用
已经很清楚了,委托就是一个回调函数的机制。在需要回调函数的地方就需要委托。委托提供了一种预计在将来某一时刻执行预定的某一特定行为的“委托”机制。
(2)委托的实质
委托的实质其实就是间接的完成指定的某种操作,间接执行某种行为。
(3)编译器和CLR如何实现委托,委托对象如何构造,委托对象的内部结构
每一个委托类型都是继承于System.MulticastDelegate类(注:MulticastDelegate类派生于Delegate类,System.Delegate类又派生于System.Object类)。在执行委托类型的时候,C#编译器和CLR就会对所声明的委托类型创建一个名称相同的完整的类并继承于System.MulticastDelegate类。C#编译器和CLR为所声明的委托类型定义的类型具有的成员有:
一个和所声明的委托类型的名称相同的(也和类的名称相同)类的构造器;
一个和已经定义的方法(原方法)指定的类型一样的方法Invoke,负责调用已经定义的方法也就是原方法;
两个实现了对回调方法进行异步回调的方法BeginInvoke()和EndInvoke()。
因为所有的委托类型都继承于System.MulticastDelegate类,因此所有的委托类型也具有System.MulticastDelegate类中声明的字段和方法。System.MulticastDelegate类中声明的重要的三个字段和一个实例方法有:
1、_target System.Object类型:表示引用回调方法要操作的对象,就是定义回调方法的类的对象。当委托类型封装的是静态方法时,_target值为null。当封装的是实例方法时_target就是回调方法表示所要操作的对象的this的值;
2、_methodPtr System.IntPtr类型:表示委托类型所要封装的回调方法;
3、_invocationList System.Object类型:表示引用的委托数组,每个委托实例的调用列表。当委托链只有一个委托实例时_invocationList值为null
4、GetInvocationList()方法:签名为public sealed override Delegate[] GetInvocationList(),表示显示调用委托链中的每个委托实例。使用GetInvocationList()方法,需要创建指定类型的委托实例。返回委托链的委托实例数组。
(4)委托的工作方式(委托如何回调方法)
定义需要委托实例包装的方法:
pulic void ConsoleToHello(string value)
{
console.writeLine("value="+value);
}
定义委托:
public delegate void Feedback(string value);
创建委托实例并执行:
Feedback fb=new Feedback(ConsoleToHello);
fb("Hello World !");
将新创建的委托实例(fb)像一个方法一样进行调用就可以执行该委托实例所封装、包装的方法,也就是说fb("Hello World !")语句等同于ConsoleToHello("Hello World !")。要执行委托实例(fb)所包装的方法(行为),就必须执行、生成语句ConsoleToHello("Hello World !")。
那么,fb("Hello World !")在C#编译器里面是如何转换成ConsoleToHello("Hello World !")?
委托实例是如何找到它所封装的方法(行为)?
根据委托实例的内部结构即可知,所声明的委托在C#编译器里面都会生成一个表示该委托类型的类。该类中包含的成员有_target和_methodPtr分别就表示创建委托实例所包装的方法的操作对象和实例包装的原方法。委托实例通过这两个属性就可以找到委托实例所封装的方法。而在使用委托实例进行调用和执行原方法(即委托实例所包装的方法)的时候,所生成的代码都会先调用该委托实例从System.MulticastDelegate类继承的方法Invoke()来调用该委托实例_target和_methodPtr属性所指向的委托实例包装的方法和该方法所要操作的对象(静态方法时,该对象为null)。
Invoke()方法的伪代码如下:
注:Invoke()方法和委托实例所包装的方法签名一致,也和委托实例的类型的签名一致。
private void Invoke(string value)
{
Delegate[] deleArray = this._invocationList as Delegate[];
if(deleArray!=null)
{
foreach(Feedback fb as deleArray)
{
fb(value);
}
}
else
{
_methodPtr.Invoke(_target,value);
}
}
fb("Hello World !")执行的过程如下图:
图1:委托实例的调用过程
3、委托绑定的方法、委托的特点
委托实例所包装的方法有静态方法和实例方法,使用静态方法和实例方法所创建的委托实例并没有什么不同,只是在初始化委托实例的属性_target和使用实例方法初始化委托实例需先创建实例对象会有不同而已。静态方法创建的委托实例的_target属性值为null,而实例方法所创建的委托实例的_target属性值为包装方法所要操作的对象。
委托的特点有:
(1)委托是不可变的,一旦定义一个委托实例后,委托实例就是不可变的这点和string一样;
(2)委托实例是不可被回收的,所以就会造成委托实例所封装的方法所要操作的对象也不会被回收,从而造成内存的泄漏;
(3)委托类型是引用类型,类是于非托管C/C++中的函数指针,但是委托类型是安全的;
(4)委托允许按顺序调用多个方法(就是委托链的使用)。
4、委托链的合并和删除以及局限性
委托链的合并和删除可以使用System.Delegate类的Combine()方法和Remove()方法。例如:
Feedback fbChian=null;
fbChian=(Feedback)Delegate.Combine(fbChian,fb1);
fbChian=(Feedback)Delegate.Remove(fbChian,fb1);
也可以使用更加简洁的语法,使用+和-操作符,如下:
fbChian+=fb1;
fbChian-=fb1;
注意:委托链每次合并和删除操作都会生成一个新的委托实例对象,然后原来的委托实例就可以进行垃圾回收。
委托链的局限性:
如果委托链里面的委托实例数组当中每个委托实例都具有一个返回值,那么委托链执行完后就只返回最后一个委托实例的返回值。
要想获得委托实例数组(也就是委托链的调用列表)的更多的控制,可以使用委托实从System.MulticastDelegate类继承的GetInvocationList()方法。从GetInvocationList()方法活动委托链的调用列表,然后依次循环迭代该列表或者去返回的值。
5、委托中的协变性和逆变性
协变性:是对针对方法的返回值类型,在委托中的协变性指的是用于创建和初始化委托实例的方法的返回类型可以是和声明委托类型的返回值类型一样或者是该返回类型的派生类。也就是说协变性是类型向下变化的。
逆变性:是针对方法的输入参数的类型,在委托中的逆变性指的是方法的输入参数可以是声明委托类型的输入参数类型一样或者是该输入参数的基类。也就是说逆变性是类型向上变化的。
6、委托和事件
事件的基本思想就是:让代码发生某事的时候做出响应,执行相关的事件处理程序。事件和属性类似因为:
1、都封装了数据,不过属性封装的是字段数据,而事件封装的是事件的处理程序也就是方法;
2、使用属性时可以进行赋值和取值(get,set),而使用事件时可以进行订阅和取消订阅(add,remove);
3、都添加了封装层,使得外部不能随意的更改数据和事件的处理程序,但是向外提供访问内部数据的接口。
事件和属性不同的是事件的类型必须是委托类型,但是事件并不是委托类型的实例或者说不是委托类型的字段。
7、委托和反射
(反射相关的内容忘了较多,等以后复习到了再补上)
8、委托和泛型(泛型委托)
.NET Framwork 提供了17个Action没有返回值的委托和17个Func具有返回值的委托。《CLR Via C#》里面Jeffrey Richter建议尽量使用这些委托,而不是在代码中定义更多的委托类型以减少系统中的类型的数目、简化编码。
需要自定义委托类型的情况有:
(1)委托类型的输入参数要使用ref或out关键字时就需要自定义委托类型;
(2)要通过params关键字获取可变量的参数,要为委托类型的每个输入参数指定默认值,或者要对委托类型的泛型参数进行约束的情况下只能自定义委托类型。
9、委托中使用Lambda表达式
在创建委托实例时可以使用Lambda表达式以内联的方式嵌入一个匿名方法。一般在代码中只发生一次引用该代码主体的情况下会使用,而在代码中发生多次引用该代码主体情况下,还是定义一个命名的方法然后将方法的名称传入更佳!
10、委托的疑问?
(1)委托实例为什么不能被回收?
(2)委托实例为什么不可变?