细说C#:委托的简化语法,聊聊匿名方法和闭包(下)

前文:细说C#:委托的简化语法,聊聊匿名方法和闭包(上)html

0x03 使用匿名方法省略参数

好,经过上面的分析,咱们能够看到使用了匿名方法以后的确简化了咱们在使用委托时还要单独声明对应的回调函数的繁琐。那么是否可能更加极致一些,好比用在咱们在前面介绍的事件中,甚至是省略参数呢?下面咱们来修改一下咱们在事件的部分所完成的代码,看看如何经过使用匿名方法来简化它吧。编程

在以前的博客的例子中,咱们定义了AddListener来为BattleInformationComponent 的OnSubHp方法订阅BaseUnit的OnSubHp事件。segmentfault

private void AddListener()
{
    this.unit.OnSubHp += this.OnSubHp;
}

其中this.OnSubHp方法是咱们为了响应事件而单独定义的一个方法,若是不定义这个方法而改由匿名方法直接订阅事件是否能够呢?答案是确定的。闭包

private void AddListener()
{
       this.unit.OnSubHp += delegate(BaseUnit source, float subHp, DamageType damageType, HpShowType showType) 
       {
              string unitName = string.Empty;
              string missStr = "闪避";
              string damageTypeStr = string.Empty;
              string damageHp = string.Empty;

              if(showType == HpShowType.Miss)
              {
                  Debug.Log(missStr);
                  return;
              }

              if(source.IsHero)
              {
                  unitName = "英雄";
              }
              else
              {
                 unitName = "士兵";
              }
              damageTypeStr = damageType == DamageType.Critical ? "暴击" : "普通攻击" ;
              damageHp = subHp.ToString();
              Debug.Log(unitName + damageTypeStr + damageHp);

       };
}

在这里咱们直接使用了delegate关键字定义了一个匿名方法来做为事件的回调方法而无需再单独定义一个方法。可是因为在这里咱们要实现掉血的信息显示功能,于是看上去咱们须要全部传入的参数。那么在少数状况下,咱们不须要使用事件所要求的参数时,是否能够经过匿名方法在不提供参数的状况下订阅那个事件呢?答案也是确定的,也就是说在不须要使用参数的状况下,咱们经过匿名方法能够省略参数。仍是在触发OnSubHp事件时,咱们只须要告诉开发者事件触发便可,因此咱们能够将AddListener方法改成下面这样:编程语言

private void AddListener()
{
   this.unit.OnSubHp += this.OnSubHp;
   this.unit.OnSubHp += delegate {
          Debug.Log("呼救呼救,我被攻击了!");
   };
}

以后,让咱们运行一下修改后的脚本。能够在Unity3D的调试窗口看到以下内容的输出:ide

英雄暴击10000

UnityEngine.Debug:Log(Object)

呼救呼救,我被攻击了!

UnityEngine.Debug:Log(Object)

0x04 匿名方法和闭包

固然,在使用匿名方法时另外一个值得开发者注意的一个知识点即是闭包状况。所谓的闭包指的是:一个方法除了能和传递给它的参数交互以外,还能够同上下文进行更大程度的互动。函数

首先要指出闭包的概念并不是C#语言独有的。事实上闭包是一个很古老的概念,而目前不少主流的编程语言都接纳了这个概念,固然也包括咱们的C#语言。而若是要真正的理解C#中的闭包,咱们首先要先掌握另外两个概念:this

1. 外部变量设计

或者称为匿名方法的外部变量指的是定义了一个匿名方法的做用域内(方法内)的局部变量或参数对匿名方法来讲是外部变量。下面举个小例子,各位读者可以更加清晰的明白外部变量的含义:调试

int n = 0;

Del d = delegate() {
    Debug.Log(++n);
};

这段代码中的局部变量n对匿名方法来讲是外部变量。

2. 捕获的外部变量

即在匿名方法内部使用的外部变量。也就是上例中的局部变量n在匿名方法内部即是一个捕获的外部变量。

了解了以上2个概念以后,再让咱们结合闭包的定义,能够发如今闭包中出现的方法在C#中即是匿名方法,而匿名方法可以使用在声明该匿名方法的方法内部定义的局部变量和它的参数。而这么作有什么好处呢?想象一下,咱们在游戏开发的过程当中没必要专门设置额外的类型来存储咱们已经知道的数据,即可以直接使用上下文信息,这便提供了很大的便利性。那么下面咱们就经过一个小例子,来看看各类变量和匿名方法的关系吧。

using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;

public class EnclosingTest : MonoBehaviour {

       // Use this for initialization
       void Start () {
              this.EnclosingFunction(999);
       }

       // Update is called once per frame
       void Update () {
       
       }

       public void EnclosingFunction(int i)
       {
              //对匿名方法来讲的外部变量,包括参数i
              int outerValue = 100;
              //被捕获的外部变量
              string capturedOuterValue = "hello world";
              
              Action<int> anonymousMethod = delegate(int obj) {

                     //str是匿名方法的局部变量
                     //capturedOuterValue和i
                     //是匿名方法捕获的外部变量

                     string str = "捕获外部变量" + capturedOuterValue + i.ToString();
                     Debug.Log(str);
              };

              anonymousMethod(0);

              if(i == 100)
              {
                     //因为在这个做用域内没有声明匿名方法,
                     //于是notOuterValue不是外部变量
                     
                     int notOuterValue = 1000;
                     Debug.Log(notOuterValue.ToString());
              }
       }
}

好了,接下来让咱们来分析一下这段代码中的变量吧。

  • 参数i是一个外部变量,由于在它的做用域内声明了一个匿名方法,而且因为在匿名方法中使用了它,于是它是一个被捕捉的外部变量。

  • 变量outerValue是一个外部变量,这是因为在它的做用域内声明了一个匿名方法,可是和i不一样的一点是outerValue并无被匿名方法使用,于是它是一个没有被捕捉的外部变量。

  • 变量capturedOuterValue一样是一个外部变量,这也是由于在它的做用域内一样声明了一个匿名方法,可是capturedOuterValue和i同样被匿名方法所使用,于是它是一个被捕捉的外部变量。

  • 变量str不是外部变量,一样也不是EnclosingFunction这个方法的局部变量,相反它是一个匿名方法内部的局部变量。

  • 变量notOuterValue一样不是外部变量,这是由于在它所在的做用域中,并无声明匿名方法。

好了,明白了上面这段代码中各个变量的含义以后,咱们就能够继续探索匿名方法到底是如何捕捉外部变量以及捕捉外部变量的意义了。

0x05 匿名方法如何捕获外部变量

首先,咱们要明确一点,所谓的捕捉变量的背后所发生的操做的确是针对变量而言的,而不是仅仅获取变量所保存的值。这将致使什么后果呢?不错,这样作的结果是被捕捉的变量的存活周期可能要比它的做用域长,关于这一点咱们以后再详细讨论,如今的当务之急是搞清楚匿名方法是如何捕捉外部变量的。

using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;

public class EnclosingTest : MonoBehaviour {

       // Use this for initialization
       void Start () {
                  this.EnclosingFunction(999);
       }

       // Update is called once per frame
       void Update () {
      
       }

       public void EnclosingFunction(int i)
       {
              int outerValue = 100;
              string capturedOuterValue = "hello world";

              Action<int> anonymousMethod = delegate(int obj) {
                     string str = "捕获外部变量" + capturedOuterValue + i.ToString();
                     Debug.Log(str);
                     capturedOuterValue = "你好世界";
              };
              capturedOuterValue = "hello world 你好世界";

              anonymousMethod(0);

              Debug.Log(capturedOuterValue);
       }
}

将这个脚本挂载在游戏物体上,运行Unity3D能够在调试窗口看到以下的输出内容:

捕获外部变量hello world 你好世界999

UnityEngine.Debug:Log(Object)

你好世界

UnityEngine.Debug:Log(Object)

可这究竟有什么特殊的呢?看上去程序很天然的打印出了咱们想要打印的内容。

不错,这段代码向咱们展现的不是打印出的到底是什么,而是咱们这段代码从始自终都是在对同一个变量capturedOuterValue进行操做,不管是匿名方法内部仍是正常的EnclosingFunction方法内部。接下来让咱们来看看这一切到底是如何发生的。

首先咱们在EnclosingFunction方法内部声明了一个局部变量capturedOuterValue而且为它赋值为hello world。

接下来,咱们又声明了一个委托实例anonymousMethod,同时将一个内部使用了capturedOuterValue变量的匿名方法赋值给委托实例anonymousMethod,而且这个匿名方法还会修改被捕获的变量的值,须要注意的是声明委托实例的过程并不会执行该委托实例。于是咱们能够看到匿名方法内部的逻辑并无当即执行。

好了,下面咱们这段代码的核心部分要来了,咱们在匿名方法的外部修改了capturedOuterValue变量的值,接下来调用anonymousMethod。咱们经过打印的结果能够看到capturedOuterValue的值已经在匿名方法的外部被修改成了“hello world 你好世界”,而且被反映在了匿名方法的内部,同时在匿名方法内部,咱们一样将capturedOuterValue变量的值修改成了“你好世界”。委托实例返回以后,代码继续执行,接下来会直接打印capturedOuterValue的值,结果为“你好世界”。这便证实了经过匿名方法建立的委托实例不是读取变量,而且将它的值再保存起来,而是直接操做该变量。

可这究竟有什么意义呢?那么,下面咱们就举一个例子,来看看这一切究竟会为咱们在开发中带来什么好处。

仍旧回到咱们开发游戏的情景之下,假设咱们须要将一个英雄列表中攻击力低于10000的英雄筛选出来,而且将筛选出的英雄放到另外一个新的列表中。若是咱们使用List<T>,则经过它的FindAll方法即可以实现这一切。可是在匿名方法出现以前,使用FindAll方法是一件十分繁琐的事情,这是因为咱们要建立一个合适的委托,而这个过程十分繁琐,已经使FindAll方法失去了简洁的意义。于是,随着匿名方法的出现,咱们能够十分方便的经过FindAll方法来实现过滤攻击力低于10000的英雄的逻辑。下面咱们就来试一试吧。

using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;

public class DelegateTest : MonoBehaviour {
       private int heroCount;
       private int soldierCount;
 
       // Use this for initialization
       void Start () {
              List<Hero> list1 = new List<Hero>();
              list1.Add(new Hero(1, 1000f, 50f, 100f));
              list1.Add(new Hero(2, 1200f, 20f, 123f));
              list1.Add(new Hero(5, 800f, 100f, 125f));
              list1.Add(new Hero(3, 600f, 54f, 120f));
              list1.Add(new Hero(4, 2000f, 5f, 110f));
              list1.Add(new Hero(6, 3000f, 65f, 105f));

              List<Hero> list2 = this.FindAllLowAttack(list1, 50f);
              foreach(Hero hero in list2)
              {
                     Debug.Log("hero's attack :" + hero.attack);
              }
       }
 
       private List<Hero> FindAllLowAttack(List<Hero> heros, float limit)
       {
              if(heros == null)
                     return null;
              return heros.FindAll(delegate(Hero obj) {
                     return obj.attack < limit;
              });
       }

       // Update is called once per frame
       void Update () {

       }
}

看到了吗?在FindAllLowAttack方法中传入的float类型的参数limit被咱们在匿名方法中捕获了。正是因为匿名方法捕获的是变量自己,于是咱们才得到了使用参数的能力,而不是在匿名方法中写死一个肯定的数值来和英雄的攻击力作比较。这样在通过设计以后,代码结构会变得十分精巧。

0x06 局部变量的存储位置

固然,咱们以前还说过将匿名方法赋值给一个委托实例时并不会马上执行这个匿名方法内部的代码,而是当这个委托被调用时才会执行匿名方法内部的代码。那么一旦匿名方法捕获了外部变量,就有可能面临一个十分可能会发生的问题。那即是若是建立了这个被捕获的外部变量的方法返回以后,一旦再次调用捕获了这个外部变量的委托实例,那么会出现什么状况呢?也就是说,这个变量的生存周期是会随着建立它的方法的返回而结束呢?仍是继续保持着本身的生存呢?下面咱们仍是经过一个小例子来一窥究竟。

using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;

public class DelegateTest : MonoBehaviour {

       // Use this for initialization
       void Start () {
              Action<int> act = this.TestCreateActionInstance();
              act(10);
              act(100);
              act(1000);
       }

       private Action<int> TestCreateActionInstance()
       {
              int count = 0;
              Action<int> action = delegate(int number) {
                     count += number;
                     Debug.Log(count);
              };
              action(1);
              return action;
       }

       // Update is called once per frame
       void Update () {

       }
}

将这个脚本挂载在Unity3D场景中的某个游戏物体上,以后启动游戏,咱们能够看到在调试窗口的输出内容以下:

1

UnityEngine.Debug:Log(Object)

11

UnityEngine.Debug:Log(Object)

111

UnityEngine.Debug:Log(Object)

1111

UnityEngine.Debug:Log(Object)

若是看到这个输出结果,各位读者是否会感到一丝惊讶呢?

由于第一次打印出1这个结果,咱们十分好理解,由于在TestCreateActionInstance方法内部咱们调用了一次action这个委托实例,而其局部变量count此时固然是可用的。可是以后当TestCreateActionInstance已经返回,咱们又三次调用了action这个委托实例,却看到输出的结果依次是十一、1十一、111,是在同一个变量的基础上累加而获得的结果。可是局部变量不是应该和方法同样分配在栈上,一旦方法返回便会随着TestCreateActionInstance方法对应的栈帧一块儿被销毁吗?可是,当咱们再次调用委托实例的结果却表示,事实并不是如此。

TestCreateActionInstance方法的局部变量count并无被分配在栈上,相反,编译器事实上在幕后为咱们建立了一个临时的类用来保存这个变量。若是咱们查看编译后的CIL代码,可能会更加直观一些。下面即是这段C#代码对应的CIL代码。

复制代码

.class nested private auto ansi sealed beforefieldinit '<TestCreateActionInstance>c__AnonStorey0'
 extends [mscorlib]System.Object
{
.custom instance void class [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::'.ctor'() =  (01 00 00 00 ) // ....

.field  assembly  int32 count

// method line 5
.method public hidebysig specialname rtspecialname
       instance default void '.ctor' ()  cil managed
       {
           // Method begins at RVA 0x20c1
           // Code size 7 (0x7)
           .maxstack 8
           IL_0000:  ldarg.0
           IL_0001:  call instance void object::'.ctor'()
           IL_0006:  ret
       } // end of method <TestCreateActionInstance>c__AnonStorey0::.ctor

     ...

} // end of class     <TestCreateActionInstance>c__AnonStorey0

咱们能够看到这个编译器生成的临时的类的名字叫作'<TestCreateActionInstance>c__AnonStorey0',这是一个让人看上去十分奇怪,可是识别度很高的名字,咱们以前已经介绍过编译器生成的名字的特色,这里就不赘述了。仔细来分析这个类,咱们能够发现TestCreateActionInstance这个方法中的局部变量count此时是编译器生成的类'<TestCreateActionInstance>c__AnonStorey0'的一个字段:

.field  assembly  int32 count

这也就证实了TestCreateActionInstance方法的局部变量count此时被存放在另外一个临时的类中,而不是被分配在了TestCreateActionInstance方法对应的栈帧上。那么TestCreateActionInstance方法又是如何来对它的局部变量count执行操做呢?答案其实十分简单,那就是TestCreateActionInstance方法保留了对那个临时类的一个实例的引用,经过类型的实例进而操做count变量。为了证实这一点,咱们一样能够查看一下TestCreateActionInstance方法对应的CIL代码。

.method private hidebysig
       instance default class [mscorlib]System.Action`1<int32> TestCreateActionInstance ()  cil managed
{
    // Method begins at RVA 0x2090
   // Code size 35 (0x23)
   .maxstack 2
   .locals init (
          class DelegateTest/'<TestCreateActionInstance>c__AnonStorey0' V_0,
          class [mscorlib]System.Action`1<int32>      V_1)
   IL_0000:  newobj instance void class DelegateTest/'<TestCreateActionInstance>c__AnonStorey0'::'.ctor'()
   IL_0005:  stloc.0
   IL_0006:  ldloc.0
   IL_0007:  ldc.i4.0
   IL_0008:  stfld int32 DelegateTest/'<TestCreateActionInstance>c__AnonStorey0'::count
   IL_000d:  ldloc.0
   IL_000e:  ldftn instance void class DelegateTest/'<TestCreateActionInstance>c__AnonStorey0'::'<>m__0'(int32)
   IL_0014:  newobj instance void class [mscorlib]System.Action`1<int32>::'.ctor'(object, native int)
   IL_0019:  stloc.1
   IL_001a:  ldloc.1
   IL_001b:  ldc.i4.1
   IL_001c:  callvirt instance void class [mscorlib]System.Action`1<int32>::Invoke(!0)
   IL_0021:  ldloc.1
   IL_0022:  ret
} // end of method DelegateTest::TestCreateActionInstance

咱们能够发如今IL_0000行,CIL代码建立了DelegateTest/'<TestCreateActionInstance>c__AnonStorey0'类的实例,而以后使用count则所有要经过这个实例。一样,委托实例之因此能够在TestCreateActionInstance方法返回以后仍然可使用count变量,也是因为委托实例一样引用了那个临时类的实例,而count变量也和这个临时类的实例一块儿被分配在了托管堆上而不是像通常的局部变量同样被分配在栈上。所以,并不是全部的局部变量都是随方法一块儿被分配在栈上的,在使用闭包和匿名方法时必定要注意这一个很容易让人忽视的知识点。固然,关于如何分配存储空间这个问题,我以前在博文《匹夫细说C#:不是“栈类型”的值类型,从生命周期聊存储位置》 也进行过讨论,欢迎各位交流指正。

相关文章
相关标签/搜索