- 委托介绍
- 何处定义委托?
- 委托既可以封装静态方法,也可以封装实例方法,还可以封装匿名方法
- 处理发布、订阅关系的几种方式
- 委托本质
- 为什么即有Delegate类,又有MulticastDelegate类,这两个类有什么区别?
- 委托判等
- 获取委托链中各个委托的返回值
安全性:
委托相对于其它语言的回调函数,最大的好处在与其的安全性。
委托是回调函数的安全类型包装(相对于非托管程序)。C++编写的非托管程序进行回调时很容易出错(C中的函数指针只不过是一个指向存储单元的指针,我们无法说出这个指针实际指向什么,像参数和返回类型等就更无从知晓了)。由于委托的存在,托管应用程序不会出现这样的情况。委托通常用来定义响应事件的回调方法的签名。
C#中的委托类似于C或C++中的函数指针。使用委托使程序员可以将方法引用封装在委托对象内( 所以这里的“引用”不是原始内存地址,而是包装了方法的内存地址的委托实例 )。然后可以将给委托对象传递可调用所引用方法的代码,而不必在编译时知道将调用哪个方法。与C或C++中的函数指针不同,委托是面向对象、类型安全的,并且是安全的。
委托声明定义一种类型,它用一组特定的参数以及返回类型封装方法。
对于静态方法,委托对象封装要调用的方法。
对于实例方法,委托对象同时封装一个实例和该实例上的一个方法。
如果你有一个委托对象和一组适当的参数,则可以用这些参数调用该委托。
委托的一个有趣且有用的属性是: 它不知道或不关心自己引用的对象的类,任何对象都可以,只是方法的参数类型必须与委托的参数类型和返回类型相匹配。
另外一点,委托实现了发布委托的类与订阅委托的类之间实现了解耦。发布委托的类只管向外面其它类公布,我这里有这样一个“接口”,具体运行时调用时会执行成什么样子,发布委托类不管;订阅委托的类只管实现如何做就可以了,具体怎么调用到它的,订阅委托类不用管。
定义委托实际上是定义一个继承自System.MultiCastDelegate的一个类,所以定义类的地方就可以定义委托。
个人认为,如果定义了一个委托,不止一个类要发布该委托,那么该委托定义就应该放在类的外面进行定义;如果定义的委托就是给某个发布者使用的,那么直接定义在这个发布者类里面就可以了。
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- namespace DelegatePractise
- {
- #region 定义委托
- public delegate void ShowInfoHandler(string info);
- #endregion
- #region 委托发布者
- class Distributor
- {
- public ShowInfoHandler handler;
- public void show( string something)
- {
- if ( this.handler != null )
- {
- handler(something);
- }
- }
- }
- #endregion
- #region 委托订阅者
- class Subscriber
- {
- //实现订阅的实例方法
- public void InstanceMethod(string something)
- {
- Console.WriteLine("i am in instance method!" + something);
- }
- //实现订阅的静态方法
- public static void SaySomething(string something)
- {
- Console.WriteLine( "i am in static method!" + something );
- }
- }
- #endregion
- class Test
- {
- static void Main(string[] args)
- {
- Distributor distributor = new Distributor();
- Subscriber subscriber = new Subscriber();
- //委托封装实例方法
- distributor.handler += subscriber.InstanceMethod;
- //委托封装静态方法
- distributor.handler += Subscriber.SaySomething;
- //委托封装匿名方法
- distributor.handler += delegate(string noName)
- {
- Console.WriteLine("i am in a noname method!" + noName);
- };
- distributor.show("haha");
- Console.ReadLine();
- }
- }
- }
第一种方式:由第三方类来关联发布者和订阅者
在这种方式下,发布者只管发布,订阅者只管实现,关联关系在第三方类中进行。代码示例如下:
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- namespace DelegatePractise
- {
- #region 定义委托
- public delegate void ShowInfoHandler(string info);
- #endregion
- #region 委托发布者
- class Distributor
- {
- //要发布出去的东东
- public ShowInfoHandler showInfoDel;
- public void show(string something)
- {
- if ( showInfoDel != null )
- {
- showInfoDel( something );
- }
- }
- }
- #endregion
- #region 委托订阅者
- class Subscriber
- {
- //实现订阅的方法
- public void SaySomething(string something)
- {
- Console.WriteLine( something );
- }
- }
- #endregion
- class Test
- {
- static void Main(string[] args)
- {
- Distributor distributor = new Distributor();
- Subscriber subscriber = new Subscriber();
- //关联发布者和订阅者之间的订阅关系
- distributor.showInfoDel += subscriber.SaySomething;
- distributor.show("我是要显示的东东!");
- Console.ReadLine();
- }
- }
- }
从上面的例子中可看出,发布者和订阅者的关联关系是在第三方类Test中关联起来的。
如果订阅者比较少,可以使用这种方式,但是,如果订阅者很多、并且没什么规律的话,这样来关联发布者和订阅者就很累了,让订阅者自己内部来进行订阅,就轻松一些。
第二种方式:订阅者使用方法来订阅发布者发布的委托
在这种方式下,订阅者使用方法订阅发布者发布的委托,不用第三方类来进行关联发布者和订阅者。代码如下:
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- namespace DelegatePractise
- {
- #region 定义委托
- public delegate void ShowInfoHandler(string info);
- #endregion
- #region 委托发布者
- class Distributor
- {
- //要发布出去的东东
- public ShowInfoHandler showInfoDel;
- public void show(string something)
- {
- if ( showInfoDel != null )
- {
- showInfoDel( something );
- }
- }
- }
- #endregion
- #region 委托订阅者
- class Subscriber
- {
- //实现订阅的方法
- public void SaySomething(string something)
- {
- Console.WriteLine( something );
- }
- //订阅者使用方法来订阅发布者的委托
- public void SubscribeDel(Distributor dis)
- {
- dis.showInfoDel += SaySomething;
- }
- }
- #endregion
- class Test
- {
- static void Main(string[] args)
- {
- Distributor distributor = new Distributor();
- Subscriber subscriber = new Subscriber();
- //订阅者订阅发布者的委托
- subscriber.SubscribeDel(distributor);
- distributor.show("我是要显示的东东!");
- Console.ReadLine();
- }
- }
- }
第三种方式:使用static readonly字段
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- namespace DelegatePractise
- {
- #region 定义委托
- public delegate void ShowInfoHandler(string info);
- #endregion
- #region 委托发布者
- class Distributor
- {
- public void show( ShowInfoHandler handler,string something)
- {
- if ( handler != null)
- {
- handler(something);
- }
- }
- }
- #endregion
- #region 委托订阅者
- class Subscriber
- {
- //在类里面已经使用static readonly的字段的方式把委托和方法关联好了
- //注意使用这种方式时,只能使用静态的方法和静态的字段
- public static readonly ShowInfoHandler handler = new ShowInfoHandler( SaySomething );
- //实现订阅的方法
- public static void SaySomething(string something)
- {
- Console.WriteLine( something );
- }
- }
- #endregion
- class Test
- {
- static void Main(string[] args)
- {
- Distributor distributor = new Distributor();
- distributor.show(Subscriber.handler, "hello");
- Console.ReadLine();
- }
- }
- }
实际上,在上面的例子中,发布者并没有定义一个委托字段来发布一个委托,但它使用了一个带委托类型参数的方法,故我们还把它当做发布者。这种方式下,发布者只管使用委托,具体委托关联了哪些方法,它不管;委托具体如何关联,以及被调用后做什么,全部都在订阅者中进行实现,很好的实现了类与类之间的解耦。
第四种方式:订阅者使用属性代替static readonly委托字段
使用static readonly字段方式的问题在于,不管你有没有使用到这个委托,你都必须实例化。故可以优化为使用属性方式,使用到该属性时,才返回一个委托实例。具体代码如下:
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- namespace DelegatePractise
- {
- #region 定义委托
- public delegate void ShowInfoHandler(string info);
- #endregion
- #region 委托发布者
- class Distributor
- {
- public void show( ShowInfoHandler handler,string something)
- {
- if ( handler != null)
- {
- handler(something);
- }
- }
- }
- #endregion
- #region 委托订阅者
- class Subscriber
- {
- public static ShowInfoHandler Handler
- {
- get
- {
- return new ShowInfoHandler( SaySomething );
- }
- }
- //实现订阅的方法
- public static void SaySomething(string something)
- {
- Console.WriteLine( something );
- }
- }
- #endregion
- class Test
- {
- static void Main(string[] args)
- {
- Distributor distributor = new Distributor();
- distributor.show(Subscriber.Handler, "hello");
- Console.ReadLine();
- }
- }
- }
我们先来把第一种发布委托方式的代码反编译,来看看能发现什么,IL代码如下:
- .namespace DelegatePractise
- {
- .class private auto ansi beforefieldinit Distributor
- extends [mscorlib]System.Object
- {
- .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
- {
- }
- .method public hidebysig instance void show(string something) cil managed
- {
- }
- .field public class DelegatePractise.ShowInfoHandler showInfoDel
- }
- .class public auto ansi sealed ShowInfoHandler
- extends [mscorlib]System.MulticastDelegate
- {
- .method public hidebysig specialname rtspecialname instance void .ctor(object 'object', native int 'method') runtime managed
- {
- }
- .method public hidebysig newslot virtual instance class [mscorlib]System.IAsyncResult BeginInvoke(string info, class [mscorlib]System.AsyncCallback callback, object 'object') runtime managed
- {
- }
- .method public hidebysig newslot virtual instance void EndInvoke(class [mscorlib]System.IAsyncResult result) runtime managed
- {
- }
- .method public hidebysig newslot virtual instance void Invoke(string info) runtime managed
- {
- }
- }
- .class private auto ansi beforefieldinit Subscriber
- extends [mscorlib]System.Object
- {
- .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
- {
- }
- .method public hidebysig instance void SaySomething(string something) cil managed
- {
- }
- }
- .class private auto ansi beforefieldinit Test
- extends [mscorlib]System.Object
- {
- .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
- {
- }
- .method private hidebysig static void Main(string[] args) cil managed
- {
- .entrypoint
- }
- }
- }
从上面的IL代码中我们会发现比我们原先写的代码中多了一个类:ShowInfoHandler,该类正是我们定义的委托的名字,这说明定义一个委托,实际上就是定义一个类,只是在c#语言中允许我们像那样来写罢了,到了底层,还是一个类。我们能够从生成的类中发现,该类有四个方法,分别为构造函数、同步invoke方法、异步invoke方法、异步结束方法。
在构造函数中有两个参数,第一个参数为委托的target,如果关联的方法为静态方法,则此object为null,第二个参数表示关联方法对应内存中的int入口地址;
Invoke方法的参数跟我们定义委托时的方法签名密切相关,那个方法签名的参数是什么样子,这里Invoke方法的参数就是什么样子;
BeginInvoke方法的参数也是跟定义委托时的方法签名密切相关,那个方法签名的参数是什么样子,BeginInvoke方法的前几个参数就是什么样子,它的倒数第二个参数也是个异步委托的实例,表示该异步方法结束时要调用的回调函数,最后一个参数一般就是把当前调用BeginInvoke方法的委托自己传进去。
EndInvoke方法有一个参数,必须是IAsyncResult类型的,该参数含有异步调用的结果信息。另外注意,IAsyncResult类型的这个参数是必须的,委托方法签名中的in/out参数也要带进来,返回值类型则就是委托签名中的返回值类型。
另外,注意看的话,会发现委托生成的这个类是继承自System.MulticastDelegate类的, 而且生成的委托类的每个方法都标识为runtime managed,而不是cil managed,表示这个类里面究竟要干什么,在编译时是不确定的,只有到了运行时才能够得知,所以方法体内都是空的。
下面,我们来看看MulticastDelegate类中几个重要的私有字段:
1)_target,类型为System.Object,指向委托被调用时应该操作的对象。该字段用于实例方法的回调,如果委托关联的方法是静态方法,则此字段为null。如果跟反射关联起来,该字段以及_methodPtr这两个字段非常有用,Delegate类中有相应的属性来访问相应的字段。具体委托与事件的关联应用,后面会有描述。
2)_methodPtr,类型为System.Int32,标识关联方法在内存中被调用时的入口地址。
3)_prev,类型为System.MulticastDelegate,表示在委托链中上一个委托,如果本委托就是委托链的头,则此字段就是null。该字段在委托判等时非常必要。
微软刚开始设计c#语言的时候,Delegate类是用在单播模式下,MulticastDelegate类是用在多播模式下,在编译的时候,编译器如果发现该委托签名返回值是null,则编译器认为它是多播模式,自动生成类并继承自MulticastDelegate,如果该委托方法签名返回值类型不是null,则编译器认为它是单播模式,自动生成类并继承自Delegate。但是在后面的很长一段时间里,接近c#快要发布的时候,发现很多时候虽然实现委托的方法有返回值,但调用委托的时候,这些返回值并不重要,即想让有返回值的委托类型也继承自MulticastDelegate,这下可麻烦了,如果是在设计的早期发现这个问题,那么很容易就沟通解决掉了,但是,在快要发布的这个时候,如果重新去设计,就要牵涉CLR小组,C#语言小组,编译器小组等,微软想想,靠,为了这么点个小功能,让我伤筋动骨,本来不稳定的名声就在外了,这次如果改一下,如果测试又不彻底,nnd就给人留下把柄了,不划算!不划算归不划算,问题还是要处理,好在微软还可以修改自己的C#编译器,这样修改了编译器后,只要是委托,就都继承自MulticastDelegate了,MulticastDelegate自己呢就继承自Delegate,Delegate则继承自Object,这就是委托的继承体系,Delegete类目前呢,里面是还有好几个非常有用的静态方法的,后面的章节会介绍到。
上面已经说过,Delegate类继承自Object类。但是Delegate类重写了Object类的Equals()方法,比较的是委托的_target字段和_method字段是否一致,如果两个都一致,那么就返回true,如果有一个不一致,就返回false。但实际上我们的所有委托实际上都是继承自MulticastDelegate的,而MulticastDelegate又重写了Delegate的Equals()方法,它先判断两个委托的_target和_method以及_prev是否一致,如果不一致,则返回false,如果一致,则接着判断,两个委托链是否一样长,不一样长,也返回false,如果一样长,则接着判断各自_prev指向的委托(实际上也是委托链)是否相等,这样判断,一直到委托链的头。
直接上代码,关键是使用GetInvocationList()方法。