c#委托(delegate)揭秘
委托是一种定义方法签名的类型。 当实例化委托时,您可以将其实例与任何具有兼容签名的方法相关联。 您可以通过委托实例调用方法。
在表面上,委托很简单,使用new来构造委托实例。使用委托实例的变量名来调用回调函数。实际情况是编译器,CLR在幕后做了大量的工作来隐藏其复杂性,只有了解了这些幕后的东西,你才能真正的掌握它、灵活的运用它。
1、声明委托
namespace DelegateDemo { internal delegate void HelloCallBack(string name); class Program { static void Main(string[] args) { } } }
通过ildasm查看中间代码,如下
编译器自动生成一个helloCallBack的类,类里面有构造方法,回调方法Invoke,异步回调方法(BeginInvoke,EndInvoke),
它继承MulticastDelegate类,MulticastDelegate继承Delegate类,c#有两个委托类(Delegate,MulticastDelegate)是有历史原因的,原来是要合并成一个类,但快到发布时间了,合并它需要重新测试,所以Delegate就幸存下来)。
2、委托的实例化
internal delegate void HelloCallBack(string name); class Program { static void Main(string[] args) { HelloCallBack helloShow = new HelloCallBack(ShowName); Console.ReadLine(); } static void ShowName(string name) { Console.WriteLine(name); } }
中间代码如下
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint / //第一个被执行的方法被称为入口函数
// Code size 15 (0xf)
.maxstack 3 //定义函数代码所用堆栈的最大深度
.locals init ([0] class DelegateDemo.HelloCallBack helloShow) //分配一个局部变量helloShow
IL_0000: nop //如果修补操作码,则填充空间,未执行任何有意义的操作
IL_0001: ldnull //将空引用推送到计算堆栈上
IL_0002: ldftn void DelegateDemo.Program::ShowName(string) //将ShowName的函数指针(非托管指针 native int 类型)推送到计算堆栈上
IL_0008: newobj instance void DelegateDemo.HelloCallBack::.ctor(object, native int) //创建一个新对象,并将对象引用推送到计算堆栈上
IL_000d: stloc.0 //从计算堆栈的顶部弹出当前值并将其存储到索引 0 处的局部变量helloShow中
IL_000e: ret //从当前方法返回,并将返回值(如果存在)从调用方的计算堆栈推送到被调用方的计算堆栈上
} // end of method Program::Main
从中间代码我们可以看到,HelloCallBack类构造函数有两个参数,HelloCallBack::.ctor(object, native int),而我的代码是
HelloCallBack helloShow = new HelloCallBack(ShowName);只有一个参数,应该编译不过。编译器在这个地方帮我们做了一些东西,
当它知道要构造的是委托时,就会分析源代码来确定引用的是哪个对象,那个方法。对象引用传递给object,ShowName的函数指针传递给native int
3、调用回调方法
源代码如下
internal delegate void HelloCallBack(string name); class Program { static void Main(string[] args) { HelloCallBack helloShow = new HelloCallBack(ShowName); helloShow("hello"); } static void ShowName(string name) { Console.WriteLine(name); Console.ReadLine(); } }
中间代码如下
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 27 (0x1b)
.maxstack 3
.locals init ([0] class DelegateDemo.HelloCallBack helloShow)
IL_0000: nop
IL_0001: ldnull
IL_0002: ldftn void DelegateDemo.Program::ShowName(string)
IL_0008: newobj instance void DelegateDemo.HelloCallBack::.ctor(object,
native int)
IL_000d: stloc.0
IL_000e: ldloc.0 //将索引 0 处的局部变量加载到计算堆栈上
IL_000f: ldstr "hello" //把一个字符串常量装入堆栈
IL_0014: callvirt instance void DelegateDemo.HelloCallBack::Invoke(string) //对对象调用后期绑定方法,并且将返回值推送到计算堆栈上
IL_0019: nop
IL_001a: ret
} // end of method Program::Mains
helloShow("hello") 等价于 helloShow.Invoke("hello");
完整的代码如下
internal delegate void HelloCallBack(string name); class Program { static void Main(string[] args) { HelloCallBack helloShow = new HelloCallBack(ShowName); helloShow.Invoke("hello");
Console.ReadLine();
} static void ShowName(string name) { Console.WriteLine(name); } }
Invoke是怎么实现的呢,查看中间代码如下
.method public hidebysig newslot virtual instance void Invoke(string name) runtime managed { } // end of method HelloCallBack::Invoke
runtime managed 表示此方法运行时有CLR处理,我推测类似于
Delegate[] delegates = helloShow.GetInvocationList();
for (int i = 0; i < delegates.Length; i++)
{
Delegate callback = delegates[i];
MethodInfo method = callback.Method;
method.Invoke(helloShow.Target, new object[] { "hello" });
}
4、委托链
委托链是委托对象的集合,利用它,可以调用委托的所有方法
Delegate有两个公共属性
Target 获取类实例,当前委托将对其调用实例方法。(静态方法访问空)
Method 获取委托所表示的方法。
多播委托的使用如下
namespace DelegateDemo { internal delegate void HelloCallBack(string name); class Program { static void Main(string[] args) { HelloCallBack helloShow = new HelloCallBack(ShowName); helloShow += ShowCHName; helloShow.Invoke("hello"); Console.ReadLine(); } public static void ShowCHName(string name) { Console.WriteLine("你好:"+name); } public static void ShowName(string name) { Console.WriteLine(name); } } }
查看MulticastDelegate源码可知,委托链保存在private object _invocationList;
HelloCallBack helloShow = new HelloCallBack(ShowName);
_invocationList被初始化成object[],数组的第一个元素为new HelloCallBack(ShowName)委托
helloShow += ShowCHName;
+使用了运算符重载,它实际调用的是Delegate的Combine,-号实际调用的是Delegate的Remove