因为工做繁忙因此距离上一篇博客已通过去一个多月的时间了,所以决心这个周末不管如何也得写点东西出来,既是总结也是分享。那么本文主要的内容集中在了委托的使用以及内部结构(固然还有事件了,可是受制于篇幅故分为两篇文章)以及结合一部分Unity3D的设计思考。固然因为时间仓促,文中不免有一些疏漏和不许确,也欢迎各位指出,共同进步。编程
在设计模式中,有一种咱们经常会用到的设计模式——观察者模式。那么这种设计模式和咱们的主题“如何在Unity3D中使用委托”有什么关系呢?别急,先让咱们来聊一聊什么是观察者模式。设计模式
首先让咱们来看看报纸和杂志的订阅是怎么一回事:数组
若是各位读者能看明白我上面所说的报纸和杂志是如何订阅的,那么各位也就了解了观察者模式究竟是怎么一回事。除了名称不大同样,在观察者模式中,报社或者说出版者被称为“主题”(Subject),而订阅者则被称为“观察者”(Observer)。将上面的报社和订阅者的关系移植到观察者模式中,就变成了以下这样:主题(Subject)对象管理某些数据,当主题内的数据改变时,便会通知已经订阅(注册)的观察者,而已经注册主题的观察者此时便会收到主题数据改变的通知并更新,而没有注册的对象则不会被通知。安全
当咱们试图去勾勒观察者模式时,可使用报纸订阅服务,或者出版者和订阅者来比拟。而在实际的开发中,观察者模式被定义为了以下这样:架构
观察者模式:定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的全部依赖者都会收到通知并自动更新。ide
那么介绍了这么多观察者模式,是否是也该说一说委托了呢?是的,C#语言经过委托来实现回调函数的机制,而回调函数是一种颇有用的编程机制,能够被普遍的用在观察者模式中。函数
那么Unity3D自己是否有提供这种机制呢?答案也是确定的,那么和委托又有什么区别呢?下面就让咱们来聊一聊这个话题。性能
固然,不能否认Unity3D游戏引擎的出现是游戏开发者的一大福音。但不得不说的是,Unity3D的游戏脚本的架构中是存在一些缺陷的。一个很好的例子就是本节要说的围绕SendMessage和BroadcastMessage而构建的消息系统。之因此说Unity3D的这套消息系统存在缺陷,主要是因为SendMessage和BroadcastMessage过于依赖反射机制(reflection)来查找消息对应的回调函数。频繁的使用反射天然会影响性能,可是性能的损耗还并不是最为严重的问题,更加严重的问题是使用这种机制以后代码的维护成本。为何说这样作是一个很糟糕的事情呢?由于使用字符串来标识一个方法可能会致使不少隐患的出现。举一个例子:假如开发团队中某个开发者决定要重构某些代码,很不巧,这部分代码即是那些可能要被这些消息调用的方法定义的代码,那么若是方法被从新命名甚至被删除,是否会致使很严重的隐患呢?答案是yes。这种隐患的可怕之处并不在于可能引起的编译时错误,偏偏相反,这种隐患的可怕之处在于编译器可能都不会报错来提醒开发者某些方法已经被更名甚至是不存在了,面对一个可以正常的运行程序而没有警觉是最可怕的,而何时这个隐患会爆发呢?就是触发了特定的消息而找不到对应的方法的时候 ,但这时候发现问题所在每每已经太迟了。this
另外一个潜在的问题是因为使用了反射机制于是Unity3D的这套消息系统也可以调用声明为私有的方法的。可是若是一个私有方法在声明的类的内部没有被使用,那么正常的想法确定都认为这是一段废代码,由于在这个类的外部不可能有人会调用它。那么对待废代码的态度是什么呢?我想不少开发者都会选择消灭这段废代码,那么一样的隐患又会出现,可能在编译时并无问题,甚至程序也能正常运行一段时间,可是只要触发了特定的消息而没有对应的方法,那即是这种隐患爆发的时候。于是,是时候向Unity3D中的SendMessage和BroadcastMessage说拜拜了,让咱们选择C#的委托来实现本身的消息机制吧。spa
在非托管代码C/C++中也存在相似的回调机制,可是这些非成员函数的地址仅仅是一个内存地址。而这个地址并不携带任何额外的信息,例如函数的参数个数、参数类型、函数的返回值类型,于是咱们说非托管C/C++代码的回调函数不是类型安全的。而C#中提供的回调函数的机制即是委托,一种类型安全的机制。为了直观的了解委托,咱们先来看一段代码:
using UnityEngine; using System.Collections; public class DelegateScript : MonoBehaviour { //声明一个委托类型,它的实例引用一个方法 internal delegate void MyDelegate(int num); MyDelegate myDelegate; void Start () { //委托类型MyDelegate的实例myDelegate引用的方法 //是PrintNum myDelegate = PrintNum; myDelegate(50); //委托类型MyDelegate的实例myDelegate引用的方法 //DoubleNum myDelegate = DoubleNum; myDelegate(50); } void PrintNum(int num) { Debug.Log ("Print Num: " + num); } void DoubleNum(int num) { Debug.Log ("Double Num: " + num * 2); } }
下面咱们来看看这段代码作的事情。在最开始,咱们能够看到internal委托类型MyDelegate的声明。委托要肯定一个回调方法签名,包括参数以及返回类型等等,在本例中MyDelegate委托制定的回调方法的参数类型是int型,同时返回类型为void。
DelegateScript类还定义了两个私有方法PrintNum和DoubleNum,它们的分别实现了打印传入的参数和打印传入的参数的两倍的功能。在Start方法中,MyDelegate类的实例myDelegate分别引用了这两个方法,而且分别调用了这两个方法。
看到这里,不知道各位读者是否会产生一些疑问,为何一个方法可以像这样myDelegate = PrintNum; “赋值”给一个委托呢?这便不得不提C#2为委托提供的方法组转换。回溯C#1的委托机制,也就是十分原始的委托机制中,若是要建立一个委托实例就必需要同时指定委托类型和要调用的方法(执行的操做),于是刚刚的那行代码就要被改成:
new MyDelegate(PrintNum);
即使回到C#1的时代,这行建立新的委托实例的代码看上去彷佛并无让开发者产生什么很差的印象,可是若是是做为较长的一个表达式的一部分时,就会让人感受很冗繁了。一个明显的例子是在启动一个新的线程时候的表达式:
Thread th = new Thread(new ThreadStart(Method));
这样看起来,C#1中的方式彷佛并不简洁。于是C#2为委托引入了方法组转换机制,即支持从方法到兼容的委托类型的隐式转换。就如同咱们一开始的例子中作的那样。
//使用方法组转换时,隐式转换会将 //一个方法组转换为具备兼容签名的 //任意委托类型 myDelegate = PrintNum; Thread th = new Thread(Method);
而这套机制之因此叫方法组转换,一个重要的缘由就是因为重载,可能不止一个方法适用。例以下面这段代码所演示的那样:
using UnityEngine; using System.Collections; public class DelegateScript : MonoBehaviour { //声明一个委托类型,它的实例引用一个方法 delegate void MyDelegate(int num); //声明一个委托类型,它的实例引用一个方法 delegate void MyDelegate2(int num, int num2); MyDelegate myDelegate; MyDelegate2 myDelegate2; void Start () { //委托类型MyDelegate的实例myDelegate引用的方法 //是PrintNum myDelegate = PrintNum; myDelegate(50); //委托类型MyDelegate2的实例myDelegate2引用的方法 //PrintNum的重载版本 myDelegate2 = PrintNum; myDelegate(50, 50); } void PrintNum(int num) { Debug.Log ("Print Num: " + num); } void PrintNum(int num1, int num2) { int result = num1 + num2; Debug.Log ("result num is : " + result); } }
这段代码中有两个方法名相同的方法:
void PrintNum(int num)
void PrintNum(int num1, int num2)
那么根据方法组转换机制,在向一个MyDelegate或一个MyDelegate2赋值时,均可以使用PrintNum做为方法组(此时有2个PrintNum,于是是“组”),编译器会选择合适的重载版本。
固然,涉及到委托的还有它的另一个特色——委托参数的逆变性和委托返回类型的协变性。这个特性在不少文章中也有过介绍,可是这里为了使读者更加加深印象,于是要具体的介绍一下委托的这种特性。
在为委托实例引用方法时,C#容许引用类型的协变性和逆变性。协变性是指方法的返回类型能够是从委托的返回类型派生的一个派生类,也就是说协变性描述的是委托返回类型。逆变性则是指方法获取的参数的类型能够是委托的参数的类型的基类,换言之逆变性描述的是委托的参数类型。
例如,咱们的项目中存在的基础单位类(BaseUnitClass)、士兵类(SoldierClass)以及英雄类(HeroClass),其中基础单位类BaseUnitClass做为基类派生出了士兵类SoldierClass和英雄类HeroClass,那么咱们能够定义一个委托,就像下面这样:
delegate Object TellMeYourName(SoldierClass soldier);
那么咱们彻底能够经过构造一个该委托类型的实例来引用具备如下原型的方法:
string TellMeYourNameMethod(BaseUnitClass base);
在这个例子中,TellMeYourNameMethod方法的参数类型是BaseUnitClass,它是TellMeYourName委托的参数类型SoldierClass的基类,这种参数的逆变性是容许的;而TellMeYourNameMethod方法的返回值类型为string,是派生自TellMeYourName委托的返回值类型Object的,于是这种返回类型的协变性也是容许的。可是有一点须要指出的是,协变性和逆变性仅仅支持引用类型,因此若是是值类型或void则不支持。下面咱们接着举一个例子,若是将TellMeYourNameMethod方法的返回类型改成值类型int,以下:
int TellMeYourNameMethod(BaseUnitClass base);
这个方法除了返回类型从string(引用类型)变成了int(值类型)以外,什么都没有被改变,可是若是要将这个方法绑定到刚刚的委托实例上,编译器会报错。虽然int型和string型同样,都派生自Object类,可是int型是值类型,于是是不支持协变性的。这一点,各位读者在实际的开发中必定要注意。
好了,到此咱们应该对委托有了一个初步的直观印象。在本节中我带领你们直观的认识了委托如何在代码中使用,以及经过C#2引入的方法组转换机制为委托实例引用合适的方法以及委托的协变性和逆变性。那么本节就到此结束,接下来让咱们更进一步的探索委托。
让咱们从新定义一个委托并建立它的实例,以后再为该实例绑定一个方法并调用它:
internal delegate void MyDelegate(int number); MyDelegate myDelegate = new MyDelegate(myMethod1); myDelegate = myMethod2; myDelegate(10);
从表面看,委托彷佛十分简单,让咱们拆分一下这段代码:用C#中的delegate关键字定义了一个委托类型MyDelegate;使用new操做符来构造一个MyDelegate委托的实例myDelegate,经过构造函数建立的委托实例myDelegate此时所引用的方法是myMethod1,以后咱们经过方法组转换为myDelegate绑定另外一个对应的方法myMethod2;最后,用调用方法的语法来调用回调函数。看上去一切都十分简单,但实际状况是这样吗?
事实上编译器和Mono运行时在幕后作了大量的工做来隐藏委托机制实现的复杂性。那么本节就要来揭开委托究竟是如何实现的这个谜题。
下面让咱们把目光从新聚焦在刚刚定义委托类型的那行代码上:
internal delegate void MyDelegate(int number);
这行对开发者们来讲十分简单的代码背后,编译器为咱们作了哪些幕后的工做呢?
让咱们使用Refactor反编译C#程序,能够看到以下图的结果:
能够看到,编译器实际上为咱们定义了一个完整的类MyDelegate:
internal class MyDelegate : System.MulticastDelegate { //构造器 [MethodImpl(0, MethodCodeType=MethodCodeType.Runtime)] public MyDelegate(object @object, IntPtr method); // Invoke这个方法的原型和源代码指定的同样 [MethodImpl(0, MethodCodeType=MethodCodeType.Runtime)] public virtual void Invoke(int number); //如下的两个方法实现对绑定的回调函数的一步回调 [MethodImpl(0, MethodCodeType=MethodCodeType.Runtime)] public virtual IAsyncResult BeginInvoke(int number, AsyncCallback callback, object @object); [MethodImpl(0, MethodCodeType=MethodCodeType.Runtime)] public virtual void EndInvoke(IAsyncResult result); }
能够看到,编译器为咱们的MyDelegate类定义了4个方法:一个构造器、Invoke、BeginInvoke以及EndInvoke。而MyDelegate类自己又派生自基础类库中定义的System.MulticastDelegate类型,因此这里须要说明的一点是全部的委托类型都派生自System.MulticastDelegate。可是各位读者可能也会了解到在C#的基础类库中还定义了另一个委托类System.Delegate,甚至System.MulticastDelegate也是从System.Delegate派生而来,而System.Delegate则继承自System.Object类。那么为什么会有两个委托类呢?这实际上是C#的开发者留下的历史遗留问题,虽然全部咱们本身建立的委托类型都继承自MulticastDelegate类,可是仍然会有一些Delegate类的方法会被用到。最典型的例子即是Delegate类的两个静态方法Combine和Remove,而这两个方法的参数都是Delegate类型的。
public static Delegate Combine( Delegate a, Delegate b ) public static Delegate Remove( Delegate source, Delegate value )
因为咱们定义的委托类派生自MulticastDelegate而MulticastDelegate又派生自Delegate,于是咱们定义的委托类型能够做为这两个方法的参数。
再回到咱们的MyDelegate委托类,因为委托是类,于是凡是可以定义类的地方,均可以定义委托,因此委托类既能够在全局范围中定义,也能够嵌套在一个类型中定义。一样,委托类也有访问修饰符,既能够经过指定委托类的访问修饰符例如:private、internal、public等等来限定访问权限。
因为全部的委托类型都继承于MulticastDelegate类,于是它们也继承了MulticastDelegate类的字段、属性以及方法,下面列出三个最重要的非公有字段:
字段 |
类型 |
做用 |
_target |
System.Object |
当委托的实例包装一个静态方法时,该字段为null;当委托的实例包装的是一个实例方法时,这个字段引用的是回调方法要操做的对象。也就是说,这个字段的值是要传递给实例方法的隐式参数this。 |
_methodPtr |
System.IntPtr |
一个内部的整数值,运行时用该字段来标识要回调的方法。 |
_invocationList |
System.Object |
该字段的值一般为null。当构造委托链时它引用一个委托数组。 |
须要注意的一点是,全部的委托都有一个获取两个参数的构造方法,这两个参数分别是对对象的引用以及一个IntPtr类型的用来引用回调函数的句柄(IntPtr 类型被设计成整数,其大小适用于特定平台。 便是说,此类型的实例在 32 位硬件和操做系统中将是 32 位,在 64 位硬件和操做系统上将是 64 位。IntPtr 对象常可用于保持句柄。 例如,IntPtr 的实例普遍地用在 System.IO.FileStream 类中来保持文件句柄)。代码以下:
public MyDelegate(object @object, IntPtr method);
可是咱们回去看一看咱们构造委托类型新实例的代码:
MyDelegate myDelegate = new MyDelegate(myMethod1);
彷佛和构造器的参数对不上呀?那为什么编译器没有报错,而是让这段代码经过编译了呢?原来C#的编译器知道要建立的是委托的实例,于是会分析代码来肯定引用的是哪一个对象和哪一个方法。分析以后,将对象的引用传递给object参数,而方法的引用被传递给了method参数。若是myMethod1是静态方法,那么object会传递为null。而这个两个方法实参被传入构造函数以后,会分别被_target和_methodPtr这两个私有字段保存,而且_ invocationList字段会被设为null。
从上面的分析,咱们能够得出一个结论,即每一个委托对象实际上都是一个包装了方法和调用该方法时要操做的对象的包装器。
假设myMethod1是一个MyClass类定义的实例方法。那么上面那行建立委托实例myDelegate的代码执行以后,myDelegate内部那三个字段的值以下:
_target |
MyClass的实例 |
_methodPtr |
myMethod1 |
_ invocationList |
null |
假设myMethod1是一个MyClass类定义的静态方法。那么上面那行建立委托实例myDelegate的代码执行以后,myDelegate内部那三个字段的值以下:
_target |
null |
_methodPtr |
myMethod1 |
_ invocationList |
null |
这样,咱们就了解了一个委托实例的建立过程以及其内部结构。那么接下来咱们继续探索一下,是如何经过委托实例来调用回调方法的。首先咱们仍是经过一段代码来开启咱们的讨论。
using UnityEngine; using System.Collections; public class DelegateScript : MonoBehaviour { delegate void MyDelegate(int num); MyDelegate myDelegate; void Start () { myDelegate = new MyDelegate(this.PrintNum); this.Print(10, myDelegate); myDelegate = new MyDelegate(this.PrintDoubleNum); this.Print(10, myDelegate); myDelegate = null; this.Print(10, myDelegate); } void Print(int value, MyDelegate md) { if(md != null) { md(value); } else { Debug.Log("myDelegate is Null!!!"); } } void PrintNum(int num) { Debug.Log ("Print Num: " + num); } void PrintDoubleNum(int num) { int result = num + num; Debug.Log ("result num is : " + result); } }
编译而且运行以后,输出的结果以下:
Print Num:10 result num is : 20 myDelegate is Null!!!
咱们能够注意到,咱们新定义的Print方法将委托实例做为了其中的一个参数。而且首先检查传入的委托实例md是否为null。那么这一步是不是画蛇添足的操做呢?答案是否认的,检查md是否为null是必不可少的,这是因为md仅仅是可能引用了MyDelegate类的实例,但它也有多是null,就像代码中的第三种状况所演示的那样。通过检查,若是md不是null,则调用回调方法,不过代码看上去彷佛是调用了一个名为md,参数为value的方法:md(value);但事实上并无一个叫作md的方法存在,那么编译器是如何来调用正确的回调方法的呢?原来编译器知道md是引用了委托实例的变量,于是在幕后会生成代码来调用该委托实例的Invoke方法。换言之,上面刚刚调用回调函数的代码md(value);被编译成了以下的形式:
md.Invoke(value);
为了更深一步的观察编译器的行为,咱们将编译后的代码反编译为CIL代码。而且截取其中Print方法部分的CIL代码:
// method line 4 .method private hidebysig instance default void Print (int32 'value', class DelegateScript/MyDelegate md) cil managed { // Method begins at RVA 0x20c8 // Code size 29 (0x1d) .maxstack 8 IL_0000: ldarg.2 IL_0001: brfalse IL_0012 IL_0006: ldarg.2 IL_0007: ldarg.1 IL_0008: callvirt instance void class DelegateScript/MyDelegate::Invoke(int32) IL_000d: br IL_001c IL_0012: ldstr "myDelegate is Null!!!" IL_0017: call void class [mscorlib]System.Console::WriteLine(string) IL_001c: ret } // end of method DelegateScript::Print
分析这段代码,咱们能够发如今IL_0008这行,编译器为咱们调用了DelegateScript/MyDelegate::Invoke(int32)方法。那么咱们是否能够显式的调用md的Invoke方法呢?答案是Yes。因此,Print方法彻底能够改为以下的定义:
void Print(int value, MyDelegate md) { if(md != null) { md.Invoke(value); } else { Debug.Log("myDelegate is Null!!!"); } }
而一旦调用了委托实例的Invoke方法,那么以前在构造委托实例时被赋值的字段_target和_methodPtr在此时便派上了用场,它们会为Invoke方法提供对象和方法信息,使得Invoke可以在指定的对象上调用包装好的回调方法。OK,本节讨论了编译器如何在幕后为咱们生成委托类、委托实例的内部结构以及如何利用委托实例的Invoke方法来调用一个回调函数,那么咱们接下来继续来讨论一下如何使用委托来回调多个方法。
为了方便,咱们将用委托调用多个方法简称为委托链。而委托链是委托对象的集合,能够利用委托链来调用集合中的委托所表明的所有方法。为了使各位可以更加直观的了解委托链,下面咱们经过一段代码来做为演示:
using UnityEngine; using System; using System.Collections; public class DelegateScript : MonoBehaviour { delegate void MyDelegate(int num); void Start () { //建立3个MyDelegate委托类的实例 MyDelegate myDelegate1 = new MyDelegate(this.PrintNum); MyDelegate myDelegate2 = new MyDelegate(this.PrintDoubleNum); MyDelegate myDelegate3 = new MyDelegate(this.PrintTripleNum); MyDelegate myDelegates = null; //使用Delegate类的静态方法Combine myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate1); myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate2); myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate3); //将myDelegates传入Print方法 this.Print(10, myDelegates); } void Print(int value, MyDelegate md) { if(md != null) { md(value); } else { Debug.Log("myDelegate is Null!!!"); } } void PrintNum(int num) { Debug.Log ("1 result Num: " + num); } void PrintDoubleNum(int num) { int result = num + num; Debug.Log ("2 result num is : " + result); } void PrintTripleNum(int num) { int result = num + num + num; Debug.Log ("3 result num is : " + result); } }
编译而且运行以后(将该脚本挂载在某个游戏物体上,运行Unity3D便可),能够看到Unity3D的调试窗口打印出了以下内容:
1 result Num: 10 2 result Num: 20 3 result Num: 30
换句话说,一个委托实例myDelegates中调用了三个回调方法PrintNum、PrintDoubleNum以及PrintTripleNum。下面,让咱们来分析一下这段代码。咱们首先构造了三个MyDelegate委托类的实例,并分别赋值给myDelegate一、myDelegate二、myDelegate3这三个变量。而以后的myDelegates初始化为null,即代表了此时没有要回调的方法,以后咱们要用它来引用委托链,或者说是引用一些委托实例的集合,而这些实例中包装了要被回调的回调方法。那么应该如何将委托实例加入到委托链中呢?不错,前文提到过基础类库中的另外一个委托类Delegate,它有一个公共静态方法Combine是专门来处理这种需求的,因此接下来咱们就调用了Delegate.Combine方法将委托加入到委托链中。
myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate1); myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate2); myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate3);
在第一行代码中,因为此时myDelegates是null,于是当Delegate.Combine方法发现要合并的是null和一个委托实例myDelegate1时,Delegate.Combine会直接返回myDelegate1的值,于是第一行代码执行完毕以后,myDelegates如今引用了myDelegate1所引用的委托实例。
当第二次调用Delegate.Combine方法,继续合并myDelegates和myDelegate2的时候,Delegate.Combine方法检测到myDelegates已经再也不是null而是引用了一个委托实例,此时Delegate.Combine方法会构建一个不一样于myDelegates和myDelegate2的新的委托实例。这个新的委托实例天然会对上文经常提起的_target和_methodPtr这两个私有字段进行初始化,可是此时须要注意的是,以前一直没有实际值的_invocationList字段此时被初始化为一个对委托实例数组的引用。该数组的第一个元素即是包装了第一个委托实例myDelegate1所引用的PrintNum方法的一个委托实例(即myDelegates此时所引用的委托实例),而数组的第二个元素则是包装了第二个委托实例myDelegate2所引用的PrintDoubleNum方法的委托实例(即myDelegate2所引用的委托实例)。以后,将这个新建立的委托实例的引用赋值给myDelegates变量,此时myDelegates指向了这个包装了两个回调方法的新的委托实例。
接下来,咱们第三次调用了Delegate.Combine方法,继续将委托实例合并到一个委托链中。此次编译器内部发生的事情和上一次大同小异,Delegate.Combine方法检测到myDelegates已经引用了一个委托实例,一样地,此次仍然会建立一个新的委托实例,新委托实例中的那两个私有字段_target和_methodPtr一样会被初始化,而_invocationList字段此时一样被初始化为一个对委托实例数组的引用,只不过此次的元素多了一个包装了第三个委托实例myDelegate3中所引用的PrintDoubleNum方法的委托实例(即myDelegate3所引用的委托实例)。以后,将这个新建立的委托实例的引用赋值给myDelegates变量,此时myDelegates指向了这个包装了三个回调方法的新的委托实例。而上一次合并中_invocationList字段所引用的委托实例数组,此时再也不须要,于是能够被垃圾回收。
当全部的委托实例都合并到一个委托链中,而且myDelegates变量引用了该委托链以后,咱们将myDelegates变量做为参数传入Print方法中,正如前文所述,此时Print方法中的代码会隐式的调用MyDelegate委托类型的实例的Invoke方法,也就是调用myDelegates变量所引用的委托实例的Invoke方法。此时Invoke方法发现_invocationList字段已经再也不是null而是引用了一个委托实例的数组,所以会执行一个循环来遍历该数组中的全部元素,并按照顺序调用每一个元素(委托实例)中包装的回调方法。因此,PrintNum方法首先会被调用,紧跟着的是PrintDoubleNum方法,最后则是PrintTripleNum方法。
有合并,对应的天然就有拆解。于是Delegate除了提供了Combine方法用来合并委托实例以外,还提供了Remove方法用来移除委托实例。例如咱们想移除包装了PrintDoubleNum方法的委托实例,那么使用Delegate.Remove的代码以下:
myDelegates = (MyDelegate)Delegate.Remove(myDelegates, new MyDelegate(PrintDoubleNum));
当Delegate.Remove方法被调用时,它会从后向前扫描myDelegates所引用的委托实例中的委托数组,而且对比委托数组中的元素的_target字段和_methodPtr字段的值是否与第二个参数即新建的MyDelegate委托类的实例中的_target字段和_methodPtr字段的值匹配。若是匹配,且删除该元素以后,委托实例数组中只剩余一个元素,则直接返回该元素(委托实例);若是删除该元素以后,委托实例数组中还有多个元素,那么就会建立一个新的委托实例,这个新建立的委托实例的_invocationList字段会引用一个由删除了目标元素以后剩余的元素所组成的委托实例数组,以后返回该委托实例的引用。固然,若是删除匹配实例以后,委托实例数组变为空,那么Remove就会返回null。须要注意的一点是,Remove方法每次仅仅移除一个匹配的委托实例,而不是删除全部和目标委托实例匹配的委托实例。
固然,若是每次合并委托和删除委托都要写Delegate.Combine和Delegate. Remove则未免显得太过繁琐,因此为了方便使用C#语言的开发者,C#编译器为委托类型的实例重载了+=和-+操做符来对应Delegate.Combine和Delegate. Remove。具体的例子,咱们能够看看下面的这段代码。
using UnityEngine; using System.Collections; public class MulticastScript : MonoBehaviour { delegate void MultiDelegate(); MultiDelegate myMultiDelegate; void Start () { myMultiDelegate += PowerUp; myMultiDelegate += TurnRed; if(myMultiDelegate != null) { myMultiDelegate(); } } void PowerUp() { print ("Orb is powering up!"); } void TurnRed() { renderer.material.color = Color.red; } }
好,我想到此我已经回答了本小节题目中所提出的那个问题:委托是如何调用多个方法的。可是为了要实现观察者模式甚至是咱们本身的消息系统,还有一个大人物不得不介绍,那就是和委托关系密切的事件,那么下一篇博客就让咱们走进委托和事件的世界中吧。