接上文:《庖丁解牛聊委托,那些编译器藏的和U3D给的(上)》
0x04 委托是如何实现的
让我们重新定义一个委托并创建它的实例,之后再为该实例绑定一个方法并调用它:
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类的字段、属性以及方法,下面列出三个最重要的非公有字段:
需要注意的一点是,所有的委托都有一个获取两个参数的构造方法,这两个参数分别是对对象的引用以及一个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内部那三个字段的值如下:
假设myMethod1是一个MyClass类定义的静态方法。那么上面那行创建委托实例myDelegate的代码执行之后,myDelegate内部那三个字段的值如下:
这样,我们就了解了一个委托实例的创建过程以及其内部结构。那么接下来我们继续探索一下,是如何通过委托实例来调用回调方法的。首先我们还是通过一段代码来开启我们的讨论。
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方法来调用一个回调函数,那么我们接下来继续来讨论一下如何使用委托来回调多个方法。
0x05 委托是如何调用多个方法的?
为了方便,我们将用委托调用多个方法简称为委托链。而委托链是委托对象的集合,可以利用委托链来调用集合中的委托所代表的全部方法。为了使各位能够更加直观的了解委托链,下面我们通过一段代码来作为演示:
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委托类的实例,并分别赋值给myDelegate1、myDelegate2、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;
}
}
好,我想到此我已经回答了本小节题目中所提出的那个问题:委托是如何调用多个方法的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。