前言
探秘委托之前,让我们先了解一下回调函数。回调函数就是一个通过函数指针调用的函数。如果把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
.Net 中通过委托提供了回调函数的机制。
一、委托
在 .Net中,委托不仅仅是回调函数机制。委托确保回调方法是类型安全的,因为委托只允许传入兼容于委托定义的方法。委托还允许顺序调用多个方法(多播委托),并支持调用静态方法和实例方法。
在 C# 用delegate
关键字定义一个委托,并且要指定一个回调方法签名:
// 声明一个委托类型,回调方法的签名:一个 string 类型的参数,它的实例引用一个方法
internal delegate void Print(string message);
委托对象是方法的包装器,使方法能通过包装器来间接回调。包装器中包装了一个方法和调用该方法时要操作的对象。
理解:委托是一个可以指向方法的类型,调用委托变量时,执行的就是变量所指向的方法
在构造委托对象的实例时,编译器会确保传入的方法的签名兼容于委托定义的签名。
public class TestStatic
{
public static void Call(string name)
{
System.Console.WriteLine($"呼叫{name}");
}
}
public class TestInstance
{
public void Call(string name)
{
System.Console.WriteLine($"呼叫{name}");
}
}
委托同样使用 new 操作符构造委托实例,调用方式就像调用方法一样:
var print = new Print(TestStatic.Call); // 传入一个兼容 Print 委托的静态方法
print("张三"); // 调用委托 ==> 调用委托变量所指向的方法 Call
1. 原理揭秘
反编译代码,可以看到委托被编译器定义成了一个完整的类:
代码结构类似这样(实际不能这样定义):
internal class Print : System.MulticastDelegate
{
/// <summary>
///
/// </summary>
/// <param name="object">对象的引用</param>
/// <param name="method">引用了回调方法的整数</param>
public Print(object @object, IntPtr method)
{
this._target = @object;
this._methodPtr= method;
this._invocationList = null;
}
// Invoke方法的签名和委托的签名匹配
public virtual void Invoke(string message)
{
}
// BeginInvoke、EndInvoke方法实现对回调方法的异步调用
public virtual IAsyncResult BeginInvoke(string message, AsyncCallback callback, object @object)
{
}
public virtual void EndInvoke(IAsyncResult result)
{
}
}
所有的委托都有一个构造器,它们都如上所示构造器取两个参数:一个是对象引用,一个是引用了回调方法的整数。
因为所有的委托类型都派生自System.MulticastDelegate
,MulticastDelegate
又派生自Delegate
,所以委托都继承了MulticastDelegate
的字段、属性和方法。 其中三个重要的非公共字段_target
、_methodPtr
、_invocationList
。就像上面代码演示的那样:
字段 | 类型 | 说明 |
---|---|---|
_target | System.Object | 1. 当委托对象包装一个静态方法时,这个字段为 null 2. 当委托对象包装一个实例方法时,这个字段引用的是回调方法要操作的对象 |
_methodPtr | System.IntPtr | 一个内部的整数值,CLR 用它标识要回调的方法 |
_target | System.Object | 通常为 null,构造委托链时将引用一个委托数组 |
C#编译器知道要构造的是委托,所以会分析源代码来确定引用的是哪个对象和方法。对象引用被传给构造器的 object 参数,标识了方法的一个特殊 IntPtr 值(从 MethodDef 或 MethodRef 元数据 token 获得)被传给构造器的 method 参数。
包装静态方法和实例方法的委托对象如下:
在回到上面案例:
var print = new Print(TestStatic.Call);
print("张三"); // 也可以直接 print.Invoke("张三");
编译器知道 print 是引用了委托对象的变量,所以会生成代码调用 Print 实例的 Invoke 方法(Invoke 方法的签名和委托的签名匹配),就像这样:
print.Invoke("张三");
2. 委托链
委托链是委托对象的集合,可利用委托链调用集合中的委托所代表的的全部方法。Delegate 类的公共静态方法 Combine
可以将委托添加到链中:
var d1 = new Print(TestStatic.Call);
TestInstance instance1 = new TestInstance();
var d2 = new Print(instance1.Call);
TestInstance instance2 = new TestInstance();
var d3 = new Print(instance2.Call);
Delegate? delegates = Delegate.Combine(d1); // Combine1
delegates = Delegate.Combine(delegates, d2); // Combine2
delegates = Delegate.Combine(delegates, d3); // Combine3
如上面,执行 Combine1 时,Combine 方法发现试图合并的是 null 和委托 d1,在内部 Combine 会直接返回 d1 中的值,这时 delegates 变量同样指向 d1 所指向的委托对象。
执行 Combine2 时,Combine 内部发现 delegates 已经引用了一个委托对象,Combine 会构造一个新的委托对象,新委托对象会默认初始化_target
和_methodPtr
(内部初始化值,这里不用考虑),这时_invocationList
字段不在为 null,而是被初始化为引用一个委托对象数组。数组的第0、1位元素分别被初始化为引用包装方法的委托 d1、d2。最后 delegates 变量指向新构造的委托对象。
指行 Combine3 时,同上面类似,Combine 同样会构造一个新的委托对象,并对新的委托对象进行初始化。_invocationList
同样是被初始化为引用一个委托对象数组,并把数组中的元素指向对应的委托对象,然后 delegates 变量重新指向新构造的委托对象。最后之前 Combine2 新建的委托以及其_invocationList
字段引用的数组可以进行垃圾回收。
可以加向委托链添加委托,同样的也可以调用 Delegate 的公共静态方法 Remove 从链中删除一个委托:
delegates = Delegate.Remove(delegates, d1); // 从 delegates 委托 _invocationList 数组中删除委托 d1
Remove 方法内部会扫描 delegates 委托对象内部维护的委托数组(从末尾向索引0扫描),然后进行删除。删除委托后有几种情况:
- 委托数组中任然有多个委托,这时候构建一个新的委托,并初始化委托数组
- 委托数组中只有一个委托,则直接返回唯一委托的引用(把 delegates 指向这个委托)
- 一个委托也不剩,返回 null
委托链的调用有其局限性。如果是有返回值的委托,委托链的调用如下(伪代码):
internal delegate string Print(string message); // 有返回值的委托
public string Invoke(string message)
{
string result;
Delegate[] delegateSet = _invocationList as Delegate[];
if (delegateSet != null) // 是委托链
{
foreach (Print p in delegateSet)
result = p(message); // 遍历调用每个委托
}
else // 不是委托链
{
result = _methodPtr.Invoke(_target, message);
}
// 当然可能还有一些非空判断
return result;
}
委托链调用时,数组中每个委托都会被调用,并把结果保存到 result 变量中。循环结束后只包含最后一次调用的结果,然后把最后一次调用的结果返回给 Invoke 调用者。
局限不仅仅是其他返回结果会被丢弃,因为委托数组是遍历顺序调用,那么如果有一个委托抛异常或者阻塞,将会影响后续所有委托对象的调用。
GetInvocationList
MulticastDelegate 类以及其基类 Delegate 提供了实例方法 GetInvocationList
可以用于显示获取委托链中的每个委托,你可以自己定义每个委托的调用方式。
GetInvocationList
内部会构造并初始化一个 Delegate 组,数组每一个元素都引用链中的一个委托,最后返回对该数组的引用。
if (delegates != null)
{
Delegate[] delegateSet = delegates.GetInvocationList();
foreach (Print p in delegateSet)
{
p("张三");
}
}
非必要建议直接使用 Microsoft 定义好的委托Func
、Action