匹夫细说C#:委托的简化语法,聊聊匿名方法和闭包
0x00 前言
\n通过上一篇博客《匹夫细说C#:庖丁解牛聊委托,那些编译器藏的和u3d给的》的内容,我们实现了使用委托来构建我们自己的消息系统的过程。但是在日常的开发中,仍然有很多开发者因为这样或那样的原因而选择疏远委托,而其中最常见的一个原因便是因为委托的语法奇怪而对委托产生抗拒感。
\n因而本文的主要目标便是介绍一些委托的简化语法,为有这种心态的开发者们减轻对委托的抗拒心理。
0x01 不必构造委托对象
\n\n委托的一种常见的使用方式,就像下面的这行代码一样:
\n\n\nthis.unit.OnSubHp += new BaseUnit.SubHpHandler(this.OnSubHp);\n
其中括号中的OnSubHp是方法,该方法的定义如下:
\n\n\nprivate void OnSubHp (BaseUnit source, float subHp, DamageType damageType, HpShowType showType)\n {\n string unitName = string.Empty;\n string missStr = "闪避";\n string damageTypeStr = string.Empty;\n string damageHp = string.Empty;\n if(showType == HpShowType.Miss)\n {\n Debug.Log(missStr);\n return;\n }\n \n if(source.IsHero)\n {\n unitName = "英雄";\n }\n else\n {\n unitName = "士兵";\n }\n damageTypeStr = damageType == DamageType.Critical ? "暴击" : "普通攻击" ;\n damageHp = subHp.ToString();\n Debug.Log(unitName + damageTypeStr + damageHp);\n }\n上面列出的第一行代码的意思是向this.unit的OnSubHp事件登记方法OnSubHp的地址,当OnSubHp事件被触发时通知调用OnSubHp方法。而这行代码的意义在于,通过构造SubHpHandler委托类型的实例来获取一个将回调方法OnSubHp进行包装的包装器,以确保回调方法只能以类型安全的方式调用。同时通过这个包装器,我们还获得了对委托链的支持。但是,更多的程序员显然更倾向于简单的表达方式,他们无需真正了解创建委托实例以获得包装器的意义,而只需要为事件注册相应的回调方法即可。例如下面的这行代码:
\n\n\nthis.unit.OnSubHp += this.OnSubHp;\n
之所以能够这样写,我在之前的博客中已经有过解释。虽然“+=”操作符期待的是一个SubHpHandler委托类型的对象,而this.OnSubHp方法应该被SubHpHandler委托类型对象包装起来。但是由于C#的编译器能够自行推断,因而可以将构造SubHpHandler委托实例的代码省略,使得代码对程序员来说可读性更强。不过,编译器在幕后却并没有什么变化,虽然开发者的语法得到了简化,但是编译器生成CIL代码仍旧会创建新的SubHpHandler委托类型实例。
\n简而言之,C#允许通过指定回调方法的名称而省略构造委托类型实例的代码。
0x02 匿名方法初探
\n在上一篇博文中,我们可以看到通常在使用委托时,往往要声明相应的方法,例如参数和返回类型必须符合委托类型确定的方法原型。而且,我们在实际的游戏开发过程中,往往也需要委托的这种机制来处理十分简单的逻辑,但对应的,我们必须要创建一个新的方法和委托类型匹配,这样做看起来将会使得代码变得十分臃肿。因而,在C#2的版本中,引入了匿名方法这种机制。什么是匿名方法?下面让我们来看一个小例子。using UnityEngine;
\nusing System.Collections;\nusing System.Collections.Generic;\nusing System;\n\npublic class DelegateTest : MonoBehaviour {\n\n // Use this for initialization\n void Start () {\n //将匿名方法用于Action<T>委托类型\n Action<string> tellMeYourName = delegate(string name) {\n string intro = "My name is ";\n Debug.Log(intro + name);\n };\n \n Action<int> tellMeYourAge = delegate(int age) {\n string intro = "My age is ";\n Debug.Log(intro + age.ToString());\n };\n \n tellMeYourName("chenjiadong");\n tellMeYourAge(26);\n }\n \n // Update is called once per frame\n void Update () {\n \n }\n}\n将这个DelegateTest脚本挂载在某个游戏场景中的物体上,运行编辑器,可以看到在调试窗口输出了如下内容。
\n\n\nUnityEngine.Debug:Log(Object)
\nMy age is 26
\nUnityEngine.Debug:Log(Object)
\npublic delegate void Action();\n\npublic delegate void Action<T1>(T1 arg1);\n\npublic delegate void Action<T1, T2>(T1 arg1, T2 arg2);\n\npublic delegate void Action<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3);\n\npublic delegate void Action<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);\n\npublic delegate void Action<T1, T2, T3, T4, T5>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5);\n
\npublic delegate TResult Func<TResult>();\npublic delegate TResult Func<T1, TResult>(T1 arg1);\npublic delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);\npublic delegate TResult Func<T1, T2, T3, TResult>(T1 arg1, T2 arg2, T3 arg3);\npublic delegate TResult Func<T1, T2, T3, T4, TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);\npublic delegate TResult Func<T1, T2, T3, T4, T5, TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5);\n
\n好了,各位读者对C#的这两个常见的泛型委托类型有了初步的了解之后,就让我们来看一看上面那段使用了匿名方法的代码吧。首先我们可以看到匿名方法的语法:先使用delegate关键字之后如果有参数的话则是参数部分,最后便是一个代码块定义对委托实例的操作。而通过这段代码,我们也可以看出一般方法体中可以做到事情,匿名函数同样可以做。而匿名方法的实现,同样要感谢编译器在幕后为我们隐藏了很多复杂度,因为在CIL代码中,编译器为源代码中的每一个匿名方法都创建了一个对应的方法,并且采用了和创建委托实例时相同的操作,将创建的方法作为回调函数由委托实例包装。而正是由于是编译器为我们创建的和匿名方法对应的方法,因而这些的方法名都是编译器自动生成的,为了不和开发者自己声明的方法名冲突,因而编译器生成的方法名的可读性很差。
\n当然,如果乍一看上面的那段代码似乎仍然很臃肿,那么能否不赋值给某个委托类型的实例而直接使用呢?答案是肯定的,同样也是我们最常使用的匿名方法的一种方式,那便是将匿名方法作为另一个方法的参数使用,因为这样才能体现出匿名方法的价值——简化代码。下面就让我们来看一个小例子,还记得List<T>列表吗?它有一个获取Action<T>作为参数的方法——ForEach,该方法对列表中的每个元素执行Action<T>所定义的操作。下面的代码将演示这一点,我们使用匿名方法对列表中的元素(向量Vector3)执行获取normalized的操作。\n
\nusing UnityEngine;\nusing System.Collections;\nusing System.Collections.Generic;\n\npublic class ActionTest : MonoBehaviour {\n \n // Use this for initialization\n void Start () {\n List<Vector3> vList = new List<Vector3>();\n vList.Add(new Vector3(3f, 1f, 6f));\n vList.Add(new Vector3(4f, 1f, 6f));\n vList.Add(new Vector3(5f, 1f, 6f));\n vList.Add(new Vector3(6f, 1f, 6f));\n vList.Add(new Vector3(7f, 1f, 6f));\n \n vList.ForEach(delegate(Vector3 obj) {\n Debug.Log(obj.normalized.ToString());\n });\n }\n \n // Update is called once per frame\n void Update () {\n \n }\n}\n\ndelegate(Vector3 obj) {\n\n Debug.Log(obj.normalized.ToString());\n\n}\n(0.4, 0.1, 0.9)
\n\n那么,匿名方法的表现形式能否更加极致的简洁呢?当然,如果不考虑可读性的话,我们还可以将匿名方法写成这样的形式:
\n\n\nvList.ForEach(delegate(Vector3 obj) {Debug.Log(obj.normalized.ToString());});\n\n除了Action<T>这种返回类型为void的委托类型之外,上文还提到了另一种委托类型,即Func<T>。所以上面的代码我们可以修改为如下的形式,使得匿名方法可以有返回值。\n
\nusing UnityEngine;\nusing System;\nusing System.Collections;\nusing System.Collections.Generic;\n\npublic class DelegateTest : MonoBehaviour {\n \n // Use this for initialization\n void Start () {\n Func<string, string> tellMeYourName = delegate(string name) {\n string intro = "My name is ";\n return intro + name;\n };\n \n Func<int, int, int> tellMeYourAge = delegate(int currentYear, int birthYear) {\n return currentYear - birthYear;\n };\n \n Debug.Log(tellMeYourName("chenjiadong"));\n Debug.Log(tellMeYourAge(2015, 1989));\n }\n \n // Update is called once per frame\n void Update () {\n \n }\n}\n可以看到,我们通过tellMeYourName和tellMeYourAge这两个委托实例分别调用了我们定义的匿名方法。
\n当然,在C#语言中,除了刚刚提到过的Action<T>和Func<T>之外,还有一些我们在实际的开发中可能会遇到的预置的委托类型,例如返回值为bool型的委托类型Predicate<T>。它的签名如下:
\npublic delegate bool Predicate<T> (T Obj);\n
\nusing UnityEngine;\nusing System;\nusing System.Collections;\nusing System.Collections.Generic;\n \npublic class DelegateTest : MonoBehaviour {\n private int heroCount;\n private int soldierCount;\n \n // Use this for initialization\n void Start () {\n List<BaseUnit> bList = new List<BaseUnit>();\n bList.Add(new Soldier());\n bList.Add(new Hero());\n bList.Add(new Soldier());\n bList.Add(new Soldier());\n bList.Add(new Soldier());\n bList.Add(new Soldier());\n bList.Add(new Hero());\n Predicate<BaseUnit> isHero = delegate(BaseUnit obj) {\n return obj.IsHero;\n };\n \n foreach(BaseUnit unit in bList)\n {\n if(isHero(unit))\n CountHeroNum();\n else\n CountSoldierNum();\n }\n Debug.Log("英雄的个数为:" + this.heroCount);\n Debug.Log("士兵的个数为:" + this.soldierCount);\n }\n \n private void CountHeroNum()\n {\n this.heroCount++;\n }\n \n private void CountSoldierNum()\n {\n this.soldierCount++;\n }\n \n // Update is called once per frame\n void Update () {\n \n }\n}\n英雄的个数为:2
\n\n当然除了过滤和匹配目标,我们常常还会碰到对列表按照某一种条件进行排序的情况。例如要对按照英雄的最大血量进行排序或者按照英雄的战斗力来进行排序等等,可以说是按照要求排序是游戏系统开发过程中最常见的需求之一。那么是否也可以通过委托和匿名方法来方便的实现排序功能呢?C#又是否为我们预置了一些便利的“工具”呢?答案仍然是肯定的。我们可以方便的通过C#提供的Comparison<T>委托类型结合匿名方法来方便的为列表进行排序。
\nComparison<T>的签名如下:
\npublic delegate int Comparison(in T)(T x, T y)\n

\n好了,现在我们已经明确了Comparison<T>委托类型的参数和返回值的意义。那么下面我们就通过定义匿名方法来使用它对英雄(Hero)列表按指定的标准进行排序吧。
\n首先我们重新定义Hero类,提供英雄的属性数据。
\nusing UnityEngine;\nusing System.Collections;\n \npublic class Hero : BaseUnit{\n public int id;\n public float currentHp;\n public float maxHp;\n public float attack;\n public float defence;\n \n public Hero()\n {\n }\n \n public Hero(int id, float maxHp, float attack, float defence)\n {\n this.id = id;\n this.maxHp = maxHp;\n this.currentHp = this.maxHp;\n this.attack = attack;\n this.defence = defence;\n }\n \n public float PowerRank\n {\n get\n {\n return 0.5f * maxHp + 0.2f * attack + 0.3f * defence;\n }\n }\n \n public override bool IsHero\n {\n get\n {\n return true;\n }\n }\n}\n\nusing System;\nusing System.Collections;\nusing System.Collections.Generic;\n \npublic class DelegateTest : MonoBehaviour {\n private int heroCount;\n private int soldierCount;\n \n // Use this for initialization\n void Start () {\n List<Hero> bList = new List<Hero>();\n bList.Add(new Hero(1, 1000f, 50f, 100f));\n bList.Add(new Hero(2, 1200f, 20f, 123f));\n bList.Add(new Hero(5, 800f, 100f, 125f));\n bList.Add(new Hero(3, 600f, 54f, 120f));\n bList.Add(new Hero(4, 2000f, 5f, 110f));\n bList.Add(new Hero(6, 3000f, 65f, 105f));\n \n //按英雄的ID排序\n this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){\n return Obj.id.CompareTo(Obj2.id);\n },"按英雄的ID排序");\n //按英雄的maxHp排序\n this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){\n return Obj.maxHp.CompareTo(Obj2.maxHp);\n },"按英雄的maxHp排序");\n //按英雄的attack排序\n this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){\n return Obj.attack.CompareTo(Obj2.attack);\n },"按英雄的attack排序");\n //按英雄的defense排序\n this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){\n return Obj.defence.CompareTo(Obj2.defence);\n },"按英雄的defense排序");\n //按英雄的powerRank排序\n this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){\n return Obj.PowerRank.CompareTo(Obj2.PowerRank);\n },"按英雄的powerRank排序");\n }\n \n public void SortHeros(List<Hero> targets ,Comparison<Hero> sortOrder, string orderTitle)\n {\n// targets.Sort(sortOrder);\n Hero[] bUnits = targets.ToArray();\n Array.Sort(bUnits, sortOrder);\n Debug.Log(orderTitle);\n foreach(Hero unit in bUnits)\n {\n Debug.Log("id:" + unit.id);\n Debug.Log("maxHp:" + unit.maxHp);\n Debug.Log("attack:" + unit.attack);\n Debug.Log("defense:" + unit.defence);\n Debug.Log("powerRank:" + unit.PowerRank);\n }\n }\n \n // Update is called once per frame\n void Update () {\n \n }\n}\n\n0x03 使用匿名方法省略参数
\n好,通过上面的分析,我们可以看到使用了匿名方法之后的确简化了我们在使用委托时还要单独声明对应的回调函数的繁琐。那么是否可能更加极致一些,比如用在我们在前面介绍的事件中,甚至是省略参数呢?下面我们来修改一下我们在事件的部分所完成的代码,看看如何通过使用匿名方法来简化它吧。
\n在之前的博客的例子中,我们定义了AddListener来为BattleInformationComponent 的OnSubHp方法订阅BaseUnit的OnSubHp事件。\n
\n private void AddListener()\n{\n this.unit.OnSubHp += this.OnSubHp;\n}\n\nprivate void AddListener()\n {\n this.unit.OnSubHp += delegate(BaseUnit source, float subHp, DamageType damageType, HpShowType showType) {\n string unitName = string.Empty;\n string missStr = "闪避";\n string damageTypeStr = string.Empty;\n string damageHp = string.Empty;\n if(showType == HpShowType.Miss)\n {\n Debug.Log(missStr);\n return;\n } \n\n if(source.IsHero)\n {\n unitName = "英雄";\n }\n else\n {\n unitName = "士兵";\n }\n damageTypeStr = damageType == DamageType.Critical ? "暴击" : "普通攻击" ;\n damageHp = subHp.ToString();\n Debug.Log(unitName + damageTypeStr + damageHp);\n };\n }\n\nprivate void AddListener()\n{\n this.unit.OnSubHp += this.OnSubHp;\n this.unit.OnSubHp += delegate {\n Debug.Log("呼救呼救,我被攻击了!");\n };\n}\n英雄暴击10000
\n\n\n当然,在使用匿名方法时另一个值得开发者注意的一个知识点便是闭包情况。所谓的闭包指的是:一个方法除了能和传递给它的参数交互之外,还可以同上下文进行更大程度的互动。
\n首先要指出闭包的概念并非C#语言独有的。事实上闭包是一个很古老的概念,而目前很多主流的编程语言都接纳了这个概念,当然也包括我们的C#语言。而如果要真正的理解C#中的闭包,我们首先要先掌握另外两个概念:
\n1.外部变量:或者称为匿名方法的外部变量指的是定义了一个匿名方法的作用域内(方法内)的局部变量或参数对匿名方法来说是外部变量。下面举个小例子,各位读者能够更加清晰的明白外部变量的含义:\n
\nint n = 0;\nDel d = delegate() {\nDebug.Log(++n);\n};\n\n2.捕获的外部变量:即在匿名方法内部使用的外部变量。也就是上例中的局部变量n在匿名方法内部便是一个捕获的外部变量。
\n了解了以上2个概念之后,再让我们结合闭包的定义,可以发现在闭包中出现的方法在C#中便是匿名方法,而匿名方法能够使用在声明该匿名方法的方法内部定义的局部变量和它的参数。而这么做有什么好处呢?想象一下,我们在游戏开发的过程中不必专门设置额外的类型来存储我们已经知道的数据,便可以直接使用上下文信息,这便提供了很大的便利性。那么下面我们就通过一个小例子,来看看各种变量和匿名方法的关系吧。\n
\nusing UnityEngine;\nusing System;\nusing System.Collections;\nusing System.Collections.Generic;\n \npublic class EnclosingTest : MonoBehaviour {\n \n // Use this for initialization\n void Start () {\n this.EnclosingFunction(999);\n }\n\n // Update is called once per frame\n void Update () {\n \n }\n \n public void EnclosingFunction(int i)\n {\n //对匿名方法来说的外部变量,包括参数i\n int outerValue = 100;\n //被捕获的外部变量\n string capturedOuterValue = "hello world";\n \n Action<int> anonymousMethod = delegate(int obj) {\n //str是匿名方法的局部变量\n //capturedOuterValue和i\n //是匿名方法捕获的外部变量\n string str = "捕获外部变量" + capturedOuterValue + i.ToString();\n Debug.Log(str);\n };\n anonymousMethod(0);\n if(i == 100)\n {\n //由于在这个作用域内没有声明匿名方法,\n //因而notOuterValue不是外部变量\n int notOuterValue = 1000;\n Debug.Log(notOuterValue.ToString());\n }\n }\n}\n- \n\t
- 参数i是一个外部变量,因为在它的作用域内声明了一个匿名方法,并且由于在匿名方法中使用了它,因而它是一个被捕捉的外部变量。 \n\t
- 变量outerValue是一个外部变量,这是由于在它的作用域内声明了一个匿名方法,但是和i不同的一点是outerValue并没有被匿名方法使用,因而它是一个没有被捕捉的外部变量。 \n\t
- 变量capturedOuterValue同样是一个外部变量,这也是因为在它的作用域内同样声明了一个匿名方法,但是capturedOuterValue和i一样被匿名方法所使用,因而它是一个被捕捉的外部变量。 \n\t
- 变量str不是外部变量,同样也不是EnclosingFunction这个方法的局部变量,相反它是一个匿名方法内部的局部变量。 \n\t
- 变量notOuterValue同样不是外部变量,这是因为在它所在的作用域中,并没有声明匿名方法。 \n
0x05 匿名方法如何捕获外部变量
\n首先,我们要明确一点,所谓的捕捉变量的背后所发生的操作的确是针对变量而言的,而不是仅仅获取变量所保存的值。这将导致什么后果呢?不错,这样做的结果是被捕捉的变量的存活周期可能要比它的作用域长,关于这一点我们之后再详细讨论,现在的当务之急是搞清楚匿名方法是如何捕捉外部变量的。\n\n\nusing UnityEngine;\nusing System;\nusing System.Collections;\nusing System.Collections.Generic;\n \npublic class EnclosingTest : MonoBehaviour {\n\n // Use this for initialization\n void Start () {\n this.EnclosingFunction(999);\n }\n \n // Update is called once per frame\n void Update () {\n \n }\n \n public void EnclosingFunction(int i)\n {\n int outerValue = 100;\n string capturedOuterValue = "hello world";\n \n Action<int> anonymousMethod = delegate(int obj) {\n string str = "捕获外部变量" + capturedOuterValue + i.ToString();\n Debug.Log(str);\n capturedOuterValue = "你好世界";\n };\n capturedOuterValue = "hello world 你好世界";\n anonymousMethod(0);\n Debug.Log(capturedOuterValue);\n }\n}\n捕获外部变量hello world 你好世界999
\n\n可这究竟有什么特殊的呢?看上去程序很自然的打印出了我们想要打印的内容。不错,这段代码向我们展示的不是打印出的究竟是什么,而是我们这段代码从始自终都是在对同一个变量capturedOuterValue进行操作,无论是匿名方法内部还是正常的EnclosingFunction方法内部。接下来让我们来看看这一切究竟是如何发生的,首先我们在EnclosingFunction方法内部声明了一个局部变量capturedOuterValue并且为它赋值为hello world。接下来,我们又声明了一个委托实例anonymousMethod,同时将一个内部使用了capturedOuterValue变量的匿名方法赋值给委托实例anonymousMethod,并且这个匿名方法还会修改被捕获的变量的值,需要注意的是声明委托实例的过程并不会执行该委托实例。因而我们可以看到匿名方法内部的逻辑并没有立即执行。好了,下面我们这段代码的核心部分要来了,我们在匿名方法的外部修改了capturedOuterValue变量的值,接下来调用anonymousMethod。我们通过打印的结果可以看到capturedOuterValue的值已经在匿名方法的外部被修改为了“hello world 你好世界”,并且被反映在了匿名方法的内部,同时在匿名方法内部,我们同样将capturedOuterValue变量的值修改为了“你好世界”。委托实例返回之后,代码继续执行,接下来会直接打印capturedOuterValue的值,结果为“你好世界”。这便证明了通过匿名方法创建的委托实例不是读取变量,并且将它的值再保存起来,而是直接操作该变量。可这究竟有什么意义呢?那么,下面我们就举一个例子,来看看这一切究竟会为我们在开发中带来什么好处。
\n仍旧回到我们开发游戏的情景之下,假设我们需要将一个英雄列表中攻击力低于10000的英雄筛选出来,并且将筛选出的英雄放到另一个新的列表中。如果我们使用List<T>,则通过它的FindAll方法便可以实现这一切。但是在匿名方法出现之前,使用FindAll方法是一件十分繁琐的事情,这是由于我们要创建一个合适的委托,而这个过程十分繁琐,已经使FindAll方法失去了简洁的意义。因而,随着匿名方法的出现,我们可以十分方便的通过FindAll方法来实现过滤攻击力低于10000的英雄的逻辑。下面我们就来试一试吧。
\nusing UnityEngine;\nusing System;\nusing System.Collections;\nusing System.Collections.Generic;\n \npublic class DelegateTest : MonoBehaviour {\n private int heroCount;\n private int soldierCount;\n \n // Use this for initialization\n void Start () {\n List<Hero> list1 = new List<Hero>();\n list1.Add(new Hero(1, 1000f, 50f, 100f));\n list1.Add(new Hero(2, 1200f, 20f, 123f));\n list1.Add(new Hero(5, 800f, 100f, 125f));\n list1.Add(new Hero(3, 600f, 54f, 120f));\n list1.Add(new Hero(4, 2000f, 5f, 110f));\n list1.Add(new Hero(6, 3000f, 65f, 105f));\n List<Hero> list2 = this.FindAllLowAttack(list1, 50f);\n foreach(Hero hero in list2)\n {\n Debug.Log("hero's attack :" + hero.attack);\n }\n }\n \n private List<Hero> FindAllLowAttack(List<Hero> heros, float limit)\n {\n if(heros == null)\n return null;\n return heros.FindAll(delegate(Hero obj) {\n return obj.attack < limit;\n });\n }\n \n // Update is called once per frame\n void Update () {\n \n }\n}\n\n0x06 局部变量的存储位置
\n当然,我们之前还说过将匿名方法赋值给一个委托实例时并不会立刻执行这个匿名方法内部的代码,而是当这个委托被调用时才会执行匿名方法内部的代码。那么一旦匿名方法捕获了外部变量,就有可能面临一个十分可能会发生的问题。那便是如果创建了这个被捕获的外部变量的方法返回之后,一旦再次调用捕获了这个外部变量的委托实例,那么会出现什么情况呢?也就是说,这个变量的生存周期是会随着创建它的方法的返回而结束呢?还是继续保持着自己的生存呢?下面我们还是通过一个小例子来一窥究竟。\n
\nusing UnityEngine;\nusing System;\nusing System.Collections;\nusing System.Collections.Generic;\n \npublic class DelegateTest : MonoBehaviour {\n \n // Use this for initialization\n void Start () {\n Action<int> act = this.TestCreateActionInstance();\n act(10);\n act(100);\n act(1000);\n }\n \n private Action<int> TestCreateActionInstance()\n {\n int count = 0;\n Action<int> action = delegate(int number) {\n count += number;\n Debug.Log(count);\n };\n action(1);\n return action;\n }\n \n // Update is called once per frame\n void Update () {\n \n }\n}\n1
\n\n如果看到这个输出结果,各位读者是否会感到一丝惊讶呢?因为第一次打印出1这个结果,我们十分好理解,因为在TestCreateActionInstance方法内部我们调用了一次action这个委托实例,而其局部变量count此时当然是可用的。但是之后当TestCreateActionInstance已经返回,我们又三次调用了action这个委托实例,却看到输出的结果依次是11、111、111,是在同一个变量的基础上累加而得到的结果。但是局部变量不是应该和方法一样分配在栈上,一旦方法返回便会随着TestCreateActionInstance方法对应的栈帧一起被销毁吗?但是,当我们再次调用委托实例的结果却表示,事实并非如此。TestCreateActionInstance方法的局部变量count并没有被分配在栈上,相反,编译器事实上在幕后为我们创建了一个临时的类用来保存这个变量。如果我们查看编译后的CIL代码,可能会更加直观一些。下面便是这段C#代码对应的CIL代码。
\n\n\n.class nested private auto ansi sealed beforefieldinit '<TestCreateActionInstance>c__AnonStorey0'\n extends [mscorlib]System.Object\n {\n .custom instance void class [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::'.ctor'() = (01 00 00 00 ) // ....\n .field assembly int32 count\n // method line 5\n .method public hidebysig specialname rtspecialname\n instance default void '.ctor' () cil managed\n {\n // Method begins at RVA 0x20c1\n // Code size 7 (0x7)\n .maxstack 8\n IL_0000: ldarg.0\n IL_0001: call instance void object::'.ctor'()\n IL_0006: ret\n } // end of method <TestCreateActionInstance>c__AnonStorey0::.ctor\n ...\n \n } // end of class <TestCreateActionInstance>c__AnonStorey0\n\n.field assembly int32 count\n
\n.method private hidebysig\n instance default class [mscorlib]System.Action`1<int32> TestCreateActionInstance () cil managed\n {\n // Method begins at RVA 0x2090\n // Code size 35 (0x23)\n .maxstack 2\n .locals init (\n class DelegateTest/'<TestCreateActionInstance>c__AnonStorey0' V_0,\n class [mscorlib]System.Action`1<int32> V_1)\n IL_0000: newobj instance void class DelegateTest/'<TestCreateActionInstance>c__AnonStorey0'::'.ctor'()\n IL_0005: stloc.0\n IL_0006: ldloc.0\n IL_0007: ldc.i4.0\n IL_0008: stfld int32 DelegateTest/'<TestCreateActionInstance>c__AnonStorey0'::count\n IL_000d: ldloc.0\n IL_000e: ldftn instance void class DelegateTest/'<TestCreateActionInstance>c__AnonStorey0'::'<>m__0'(int32)\n IL_0014: newobj instance void class [mscorlib]System.Action`1<int32>::'.ctor'(object, native int)\n IL_0019: stloc.1\n IL_001a: ldloc.1\n IL_001b: ldc.i4.1\n IL_001c: callvirt instance void class [mscorlib]System.Action`1<int32>::Invoke(!0)\n IL_0021: ldloc.1\n IL_0022: ret\n } // end of method DelegateTest::TestCreateActionInstance\n