委托
在C++中,函数指针是一个指向内存位置的指针,它不是类型安全的,我们无法判断这个指针实际指向什么,包括参数和返回类型也无从知晓。而.NET委托完全不同,委托类型是安全的,是用户自定义的存储了一系列具有相同签名和返回类型的方法的地址的自定义类型。不仅包含对方法的引用,也可以包含对多个方法的引用。
声明委托
delegate void m_CustomDelegate(string msg);
这段代码使用delegate关键字声明了一个委托类型,它的实例将会引用一个返回类型为void并且接受一个string类型参数的方法。
委托的使用
var testDel = new CustomDelegate(StaticDelegate);
//将会在控制台打印输出:msg : 回调静态方法
testDel("回调静态方法");
testDel.Invoke("回调静态方法");
static void StaticDelegate(string msg)
{
Console.WriteLine("msg : {0}", msg);
}
协变性和逆变性
- 协变性:方法可以返回从委托的返回类型派生的一个类型
- 逆变性:方法获取的参数可以是委托的参数类型的基类
示例代码:
delegate object CallBack(FileStream stream);
//正确
var callback = new CallBack(CallMethod);
static string CallMethod(Stream stream)
{
return string.Empty;
}
CallMethod的返回string类型派生自委托类型的object,符合协变性;CallMethod的参数类型Stream是委托的参数类型FileStream的基类,符合逆变性。
不符合协变性或逆变性的无法和委托绑定
delegate object CallBack(FileStream stream);
//错误:CallMethod返回类型错误
var callback = new CallBack(CallMethod);
static int CallMethod(Stream stream)
{
return 0;
}
回调实例方法
var testDel1 = new CustomDelegate(new Program().InstanceDelegate);
testDel1.Invoke("回调实例方法");
private void InstanceDelegate(string msg)
{
Console.WriteLine("msg : {0}", msg);
}
*回调实例方法,委托需要知道方法操作的是哪个对象的实例
CLR如何实现委托
使用ILDasm.exe查看生成的程序集,如下
通过查看IL代码可以知道,在声明委托CallBack的时候,编译器其实会自动生成如下的一个类:
internal class CallBack : MulticastDelegate
{
//构造器
public CallBack(Object object, IntPtr method);
//与源代码调用方法一致
public virtual void Invoke(System.IO.FileStream);
//以下两个方法实现对回调方法的异步回调
public virtual IAsyncResult BeginInvoke(System.IO.FileStream, System.AsyncCallback callback, Object object);
public virtual void EndInvoke(System.IAsyncResult result);
}
所有委托类型都派生自MulticastDelegate,所以自然继承了父类的字段、属性和方法,其中有三个私有字段尤为重要:
- _target字段(System.Object类型):当委托绑定静态方法时,这个值为null。绑定实例方法时,该字段将会引用回调方法要操作的对象
- _methodPtr字段(System.Intptr类型):内部整数值,CLR用来标记要回调的方法
- _invocationList(System.Object类型):通常为null。在构造委托链时将会引用一个委托数组
*所有委托都有一个接受两个参数(对象引用和引用回调方法的一个整数)的构造器。
C#知道要构造的类型是委托的时候,将会分析源代码以确定引用的是哪个对象和方法。对象的引用被传给构造器的object参数,从MethodDef或MemberRef元数据token获得标识了方法的一个特殊Intptr值传给构造器的method参数。对于静态方法,object参数传递null值。在构造器内部接受的这两个值会分别存在_target和_methodPtr私有字段中。而_invocationList字段也会被设置为null。
Delegate的公共实例属性:Target和Method
- Target:返回_target的值,如果为静态方法返回null
- Method:内部将_methodPtr转换成MethodInfo对象并返回
委托链(多播委托)
委托链是由委托对象构成的一个集合,利用这一点可以调用集合中的所有方法。
delegate void CustomDelegate(string msg);
CustomDelegate delegates = null;
var delegate1 = new CustomDelegate(StaticDelegate);
var delegate2 = new CustomDelegate(StaticDelegate);
var delegate3 = new CustomDelegate(StaticDelegate);
delegates += delegate1;
delegates += delegate2;
delegates += delegate3;
delegates.Invoke("多播委托");
static void StaticDelegate(string msg)
{
Console.WriteLine("msg : {0}", msg);
}
代码分析:
- 首先声明delegates并赋值为null,再声明三个变量delegate1、delegate2、delegate3
- 接着使用+=运算符可以将delegate1添加到委托链中并且返回delegate1引用的委托对象
- 再次使用+=运算符添加第二个委托,此时由于delegates已经引用了一个委托对象,所以合并操作会构造一个新的委托对象。该委托对象初始化的时候_invocationList字段将不再是null,而是引用一个委托对象的数组。0索引被初始化为delegates当前所引用的委托,1索引初始化为delegate2引用的委托。最后delegates将被设为新建的引用对象
- 当再次执行+=运算符添加委托时,将会重复上一步骤,即:构造新的委托对象,初始化_invocationList字段索引0,1,2分别引用delegate1,delegate2,delegate3引用的委托,最后delegates被设为新建的引用对象
- 最后再执行delegates.Invoke时,该委托发现私有字_invocationList不为null,便会执行一个循环遍历数组中的所有元素,并依次调用每一个方法
删除委托
delegates -= delegate2;
删除委托时的操作:
- 从末尾向0索引扫描delegates内部的委托数组
- 查找_target和_methodPtr字段与delegate2相匹配的委托将其删除
- 删除后数组仅剩一个数据项就直接返回该数据项,否则新建一个委托对象,并初始化_invocationList数组将引用原数组中的所有数据项,返回新的委托对象的引用
- 当将最后一个数据项也删除后,返回null
*每次删除只能删除与_target和_methodPtr匹配的一个委托,而不是删除与之匹配的所有委托
委托链的局限性与解决方案
- 局限:如果其中一个委托执行错误,发生异常,后续的所有委托都将无法调用。
- 使用GetInvocationList方法实现自定义遍历委托调用
CustomDelegate delegates = null;
delegates += new CustomDelegate(StaticDelegate1);
delegates += new CustomDelegate(StaticDelegate2);
delegates += new CustomDelegate(StaticDelegate3);
var index = 0;
foreach (CustomDelegate fn in delegates.GetInvocationList())
{
index++;
try
{
fn.Invoke(string.Format("第{0}个委托", index));
}
catch (Exception exp)
{
Console.WriteLine(exp.Message);
}
}
static void StaticDelegate1(string msg)
{
Console.WriteLine("msg : {0}", msg);
}
static void StaticDelegate2(string msg)
{
throw new Exception(string.Format("{0} >> 执行错误", msg));
}
static void StaticDelegate3(string msg)
{
Console.WriteLine("msg : {0}", msg);
}
不会再因为其中某个委托发生异常而卡住程序,结果如下
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。