回到目录,因而本文的主要目标便是介绍一些委

作者: 编程  发布:2019-11-09

1、 闭包的含义

首先闭包并不是针对某一特定语言的概念,而是一个通用的概念。除了在各个支持函数式编程的语言中,我们会接触到它。一些不支持函数式编程的语言中也能支持闭包(如java8之前的匿名内部类)。

在看过的对于闭包的定义中,个人觉得比较清晰的是在《JavaScript高级程序设计》这本书中看到的。具体定义如下:

闭包是指有权访问另一个函数作用域中的变量的函数

注意,闭包这个词本身指的是一种函数。而创建这种特殊函数的一种常见方式是在一个函数中创建另一个函数。

namespace Closures
{
    class ClosuresClass
    {
        static void ClosuresTest()
        {
            Console.WriteLine(GetClosureFunc()(30));
        }

        static Func<int,int> GetClosureFunc()
        {
            int val = 10;
            Func<int, int> internalAdd = x => x + val;
            Console.WriteLine(internalAdd(10));
            val = 30;
            Console.WriteLine(internalAdd(10));
            return internalAdd;
        }
    }
}

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

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

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

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

其中this.OnSubHp方法是我们为了响应事件而单独定义的一个方法,如果不定义这个方法而改由匿名方法直接订阅事件是否可以呢?答案是肯定的。

9159.com 19159.com 2

       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);



              };

       }

View Code

在这里我们直接使用了delegate关键字定义了一个匿名方法来作为事件的回调方法而无需再单独定义一个方法。但是由于在这里我们要实现掉血的信息显示功能,因而看上去我们需要所有传入的参数。那么在少数情况下,我们不需要使用事件所要求的参数时,是否可以通过匿名方法在不提供参数的情况下订阅那个事件呢?答案也是肯定的,也就是说在不需要使用参数的情况下,我们通过匿名方法可以省略参数。还是在触发OnSubHp事件时,我们只需要告诉开发者事件触发即可,所以我们可以将AddListener方法改为下面这样:

private void AddListener()

{

       this.unit.OnSubHp += this.OnSubHp;

       this.unit.OnSubHp += delegate {

              Debug.Log("呼救呼救,我被攻击了!");

       };

}

之后,让我们运行一下修改后的脚本。可以在Unity3D的调试窗口看到如下内容的输出:

英雄暴击10000

UnityEngine.Debug:Log(Object)

呼救呼救,我被攻击了!

UnityEngine.Debug:Log(Object)

  • 0x00 前言
  • 0x01 不必构造委托对象
  • 0x02 匿名方法初探
  • 0x03 使用匿名方法省略参数
  • 0x04 匿名方法和闭包
  • 0x05 匿名方法如何捕获外部变量
  • 0x06 局部变量的存储位置

2、 在C# 中使用闭包(例子选取自《C#函数式程序设计》)

下面我们通过一个简单的例子来理解C#闭包

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(GetClosureFunction()(30));
    }

    static Func<int, int> GetClosureFunction()
    {
        int val = 10;
        Func<int, int> internalAdd = x => x + val;

        Console.WriteLine(internalAdd(10));

        val = 30;
        Console.WriteLine(internalAdd(10));

        return internalAdd;
    }
}

上述代码的执行流程是Main函数调用GetClosureFunction函数,GetClosureFunction返回了委托internalAdd并被立即执行了。

输出结果依次为20、40、60

对应到一开始提出的闭包的概念。这个委托internalAdd就是一个闭包,引用了外部函数GetClosureFunction作用域中的变量val。

注意:internalAdd有没有被当做返回值和闭包的定义无关。就算它没有被返回到外部,它依旧是个闭包。

这个原理正是许多函数构建技术的基础,这种方法显然与方法重载等面向对象方法相对应。但是与方法重载不同,匿名函数的创建可以在运行时动态发生,只需受另一个函数中的一行代码触发。为使某个算法更加容易读和写而使用的特殊函数可以在调用它的方法中创建,而不是再类级别上胡乱添加函数或方法——这正是函数模块化的核心思想。

0x04 匿名方法和闭包

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

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

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

int n = 0;

Del d = delegate() {

Debug.Log(++n);

};

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

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

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

9159.com 39159.com 4

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());

              }

       }

}

View Code

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

  • 参数i是一个外部变量,因为在它的作用域内声明了一个匿名方法,并且由于在匿名方法中使用了它,因而它是一个被捕捉的外部变量。
  • 变量outerValue是一个外部变量,这是由于在它的作用域内声明了一个匿名方法,但是和i不同的一点是outerValue并没有被匿名方法使用,因而它是一个没有被捕捉的外部变量。
  • 变量capturedOuterValue同样是一个外部变量,这也是因为在它的作用域内同样声明了一个匿名方法,但是capturedOuterValue和i一样被匿名方法所使用,因而它是一个被捕捉的外部变量。
  • 变量str不是外部变量,同样也不是EnclosingFunction这个方法的局部变量,相反它是一个匿名方法内部的局部变量。
  • 变量notOuterValue同样不是外部变量,这是因为在它所在的作用域中,并没有声明匿名方法。

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

0x01 不必构造委托对象

委托的一种常见的使用方式,就像下面的这行代码一样:

this.unit.OnSubHp += new BaseUnit.SubHpHandler(this.OnSubHp);

其中括号中的OnSubHp是方法,该方法的定义如下:

9159.com 5

private void OnSubHp (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);

    }

9159.com 6

上面列出的第一行代码的意思是向this.unit的OnSubHp事件登记方法OnSubHp的地址,当OnSubHp事件被触发时通知调用OnSubHp方法。而这行代码的意义在于,通过构造SubHpHandler委托类型的实例来获取一个将回调方法OnSubHp进行包装的包装器,以确保回调方法只能以类型安全的方式调用。同时通过这个包装器,我们还获得了对委托链的支持。但是,更多的程序员显然更倾向于简单的表达方式,他们无需真正了解创建委托实例以获得包装器的意义,而只需要为事件注册相应的回调方法即可。例如下面的这行代码:

this.unit.OnSubHp += this.OnSubHp;

之所以能够这样写,我在之前的博客中已经有过解释。虽然“+=”操作符期待的是一个SubHpHandler委托类型的对象,而this.OnSubHp方法应该被SubHpHandler委托类型对象包装起来。但是由于C#的编译器能够自行推断,因而可以将构造SubHpHandler委托实例的代码省略,使得代码对程序员来说可读性更强。不过,编译器在幕后却并没有什么变化,虽然开发者的语法得到了简化,但是编译器生成CIL代码仍旧会创建新的SubHpHandler委托类型实例。

简而言之,C#允许通过指定回调方法的名称而省略构造委托类型实例的代码。

回到目录

4、 C#7对于不作为返回值的闭包的优化

如果在vs2017中编写第二节的代码。会得到一个提示,询问是否把lambda表达式(匿名函数)托转为本地函数。本地函数是c#7提供的一个新语法。那么使用本地函数实现闭包又会有什么区别呢?

如果还是第二节那样的代码,改成本地函数,查看IL代码。实际上不会发生任何变化。

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(GetClosureFunction()(30));
    }

    static Func<int, int> GetClosureFunction()
    {
        int val = 10;
        int InternalAdd(int x) => x + val;

        Console.WriteLine(InternalAdd(10));

        val = 30;
        Console.WriteLine(InternalAdd(10));

        return InternalAdd;
    }
}

但是当internalAdd不需要被返回时,结果就不一样了。

下面分别来看下匿名函数和本地函数创建不作为返回值的闭包的时候演示代码及经整理的反编译代码。

匿名函数

static void GetClosureFunction()
{
    int val = 10;
    Func<int, int> internalAdd = x => x + val;

    Console.WriteLine(internalAdd(10));

    val = 30;
    Console.WriteLine(internalAdd(10));
}

经整理的反编译代码

sealed class DisplayClass
{
    public int val;

    public int AnonymousFunction(int x)
    {
        return x + this.val;
    }
}

static void GetClosureFunction()
{
    DisplayClass displayClass = new DisplayClass();
    displayClass.val = 10;
    Func<int, int> internalAdd = displayClass.AnonymousFunction;

    Console.WriteLine(internalAdd(10));

    displayClass.val = 30;
    Console.WriteLine(internalAdd(10));
}

本地函数

class Program
{
    static void Main(string[] args)
    {
    }

    static void GetClosureFunction()
    {
        int val = 10;
        int InternalAdd(int x) => x + val;

        Console.WriteLine(InternalAdd(10));

        val = 30;
        Console.WriteLine(InternalAdd(10));
    }
}

经整理的反编译代码

// 变化点1:由原来的class改为了struct
struct DisplayClass
{
    public int val;

    public int AnonymousFunction(int x)
    {
        return x + this.val;
    }
}

static void GetClosureFunction()
{
    DisplayClass displayClass = new DisplayClass();
    displayClass.val = 10;

    // 变化点2:不再构建委托实例,直接调用值类型的实例方法
    Console.WriteLine(displayClass.AnonymousFunction(10));

    displayClass.val = 30;
    Console.WriteLine(displayClass.AnonymousFunction(10));
}

上述这两点变化在一定程度上能够带来性能的提升,所以在官方的推荐中,如果委托的使用不是必要的,更推荐使用本地函数而非匿名函数。

如果本博客描述的内容存在问题,希望大家能够提出宝贵的意见。坚持写博客,从这一篇开始。

如果一个程序设计语言能够用高阶函数解决问题,则意味着数据作用域问题已十分突出。当函数可以当成参数和返回值在函数之间进行传递时,编译器利用闭包扩展变量的作用域,以保证随时能得到所需要的数据。

0x06 局部变量的存储位置

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

9159.com 79159.com 8

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 () {



       }

}

View Code

将这个脚本挂载在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这个委托实例,却看到输出的结果依次是11、111、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#:不是“栈类型”的值类型,从生命周期聊存储位置》 也进行过讨论,欢迎各位交流指正。

阅读目录

3、 理解闭包的实现原理

我们来分析一下这段代码的执行过程。在一开始,函数GetClosureFunction内定义了一个局部变量val和一个利用lamdba语法糖创建的委托internalAdd。

第一次执行委托internalAdd 10 + 10 输出20

接着改变了被internalAdd引用的局部变量值val,再次以相同的参数执行委托,输出40。显然局部变量的改变影响到了委托的执行结果。

GetClosureFunction将internalAdd返回至外部,以30作为参数,去执行得到的结果是60,和val局部变量最后的值30是一致的。

val 作为一个局部变量。它的生命周期本应该在GetClosureFunction执行完毕后就结束了。为什么还会对之后的结果产生影响呢?

我们可以通过反编译来看下编译器为我们做的事情。

为了增加可读性,下面的代码对编译器生成的名字进行修改,并对代码进行了适当的整理。

class Program
{
    sealed class DisplayClass
    {
        public int val;

        public int AnonymousFunction(int x)
        {
            return x + this.val;
        }
    }

    static void Main(string[] args)
    {
        Console.WriteLine(GetClosureFunction()(30));
    }

    static Func<int, int> GetClosureFunction()
    {
        DisplayClass displayClass = new DisplayClass();
        displayClass.val = 10;
        Func<int, int> internalAdd = displayClass.AnonymousFunction;

        Console.WriteLine(internalAdd(10));

        displayClass.val = 30;
        Console.WriteLine(internalAdd(10));

        return internalAdd;
    }
}

编译器创建了一个匿名类(如果不需要创建闭包,匿名函数只会是与GetClosureFunction生存在同一个类中,并且委托实例会被缓存,参见clr via C# 第四版362页),并在GetClosureFunction中创建了它实例。局部变量实际上是作为匿名类中的字段存在的。

    private sealed class DisplayClass
    {
        public int val;

        public int AnonymousFunc(int x)
        {
            return x + this.val;
        }

        private static Func<int, int> GetClosureFunc()
        {
            DisplayClass displayClass = new DisplayClass();
            displayClass.val = 10;
            Func<int, int> internalAdd = displayClass.AnonymousFunc;
            Console.WriteLine(internalAdd(10));
            displayClass.val = 30;
            Console.WriteLine(internalAdd(10));
            return internalAdd;
        }
    }

0x00 前言

通过上一篇博客《匹夫细说C#:庖丁解牛聊委托,那些编译器藏的和U3D给的》的内容,我们实现了使用委托来构建我们自己的消息系统的过程。但是在日常的开发中,仍然有很多开发者因为这样或那样的原因而选择疏远委托,而其中最常见的一个原因便是因为委托的语法奇怪而对委托产生抗拒感。

因而本文的主要目标便是介绍一些委托的简化语法,为有这种心态的开发者们减轻对委托的抗拒心理。

0x00 前言

通过上一篇博客《匹夫细说C#:庖丁解牛聊委托,那些编译器藏的和U3D给的》的内容,我们实现了使用委托来构建我们自己的消息系统的过程。但是在日常的开发中,仍然有很多开发者因为这样或那样的原因而选择疏远委托,而其中最常见的一个原因便是因为委托的语法奇怪而对委托产生抗拒感。

因而本文的主要目标便是介绍一些委托的简化语法,为有这种心态的开发者们减轻对委托的抗拒心理。

回到目录

闭包是程序设计语言支持函数式设计方法的一个重要工具。

0x02 匿名方法初探

在上一篇博文中,我们可以看到通常在使用委托时,往往要声明相应的方法,例如参数和返回类型必须符合委托类型确定的方法原型。而且,我们在实际的游戏开发过程中,往往也需要委托的这种机制来处理十分简单的逻辑,但对应的,我们必须要创建一个新的方法和委托类型匹配,这样做看起来将会使得代码变得十分臃肿。因而,在C#2的版本中,引入了匿名方法这种机制。什么是匿名方法?下面让我们来看一个小例子。

using UnityEngine;

using System.Collections;

using System.Collections.Generic;

using System;



public class DelegateTest : MonoBehaviour {



       // Use this for initialization

       void Start () {

              //将匿名方法用于Action<T>委托类型

              Action<string> tellMeYourName = delegate(string name) {

                     string intro = "My name is ";

                     Debug.Log(intro + name);

              };



              Action<int> tellMeYourAge = delegate(int age) {

                     string intro = "My age is ";

                     Debug.Log(intro + age.ToString());

              };



              tellMeYourName("chenjiadong");

              tellMeYourAge(26);



       }



       // Update is called once per frame

       void Update () {



       }

}

将这个DelegateTest脚本挂载在某个游戏场景中的物体上,运行编辑器,可以看到在调试窗口输出了如下内容。

My name is chenjiadong

UnityEngine.Debug:Log(Object)

My age is 26

UnityEngine.Debug:Log(Object)

在解释这段代码之前,我需要先为各位读者介绍一下常见的两个泛型委托类型:Action<T>以及Func<T>。它们的表现形式主要如下:

public delegate void Action();

public delegate void Action<T1>(T1 arg1);

public delegate void Action<T1, T2>(T1 arg1, T2 arg2);

public delegate void Action<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3);

public delegate void Action<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);

public delegate void Action<T1, T2, T3, T4, T5>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5);

从Action<T>的定义形式上可以看到。Action<T>是没有返回值得。适用于任何没有返回值的方法。

public delegate TResult Func<TResult>();

public delegate TResult Func<T1, TResult>(T1 arg1);

public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);

public delegate TResult Func<T1, T2, T3, TResult>(T1 arg1, T2 arg2, T3 arg3);

public delegate TResult Func<T1, T2, T3, T4, TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);

public delegate TResult Func<T1, T2, T3, T4, T5, TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5);

Func<T>委托的定义是相对于Action<T>来说。Action<T>是没有返回值的方法委托,Func<T>是有返回值的委托。返回值的类型,由泛型中定义的类型进行约束。

好了,各位读者对C#的这两个常见的泛型委托类型有了初步的了解之后,就让我们来看一看上面那段使用了匿名方法的代码吧。首先我们可以看到匿名方法的语法:先使用delegate关键字之后如果有参数的话则是参数部分,最后便是一个代码块定义对委托实例的操作。而通过这段代码,我们也可以看出一般方法体中可以做到事情,匿名函数同样可以做。而匿名方法的实现,同样要感谢编译器在幕后为我们隐藏了很多复杂度,因为在CIL代码中,编译器为源代码中的每一个匿名方法都创建了一个对应的方法,并且采用了和创建委托实例时相同的操作,将创建的方法作为回调函数由委托实例包装。而正是由于是编译器为我们创建的和匿名方法对应的方法,因而这些的方法名都是编译器自动生成的,为了不和开发者自己声明的方法名冲突,因而编译器生成的方法名的可读性很差。

当然,如果乍一看上面的那段代码似乎仍然很臃肿,那么能否不赋值给某个委托类型的实例而直接使用呢?答案是肯定的,同样也是我们最常使用的匿名方法的一种方式,那便是将匿名方法作为另一个方法的参数使用,因为这样才能体现出匿名方法的价值——简化代码。下面就让我们来看一个小例子,还记得List<T>列表吗?它有一个获取Action<T>作为参数的方法——ForEach,该方法对列表中的每个元素执行Action<T>所定义的操作。下面的代码将演示这一点,我们使用匿名方法对列表中的元素(向量Vector3)执行获取normalized的操作。

using UnityEngine;

using System.Collections;

using System.Collections.Generic;



public class ActionTest : MonoBehaviour {



       // Use this for initialization

       void Start () {

              List<Vector3> vList = new List<Vector3>();

              vList.Add(new Vector3(3f, 1f, 6f));

              vList.Add(new Vector3(4f, 1f, 6f));

              vList.Add(new Vector3(5f, 1f, 6f));

              vList.Add(new Vector3(6f, 1f, 6f));

              vList.Add(new Vector3(7f, 1f, 6f));



              vList.ForEach(delegate(Vector3 obj) {

                     Debug.Log(obj.normalized.ToString());

              });

       }



       // Update is called once per frame

       void Update () {



       }

}

我们可以看到,一个参数为Vector3的匿名方法:

delegate(Vector3 obj) {

       Debug.Log(obj.normalized.ToString());

}

实际上作为参数传入到了List的ForEach方法中。这段代码执行之后,我们可以在Unity3D的调试窗口观察输出的结果。内容如下:

(0.4, 0.1, 0.9)

UnityEngine.Debug:Log(Object)

(0.5, 0.1, 0.8)

UnityEngine.Debug:Log(Object)

(0.6, 0.1, 0.8)

UnityEngine.Debug:Log(Object)

(0.7, 0.1, 0.7)

UnityEngine.Debug:Log(Object)

(0.8, 0.1, 0.6)

UnityEngine.Debug:Log(Object)

那么,匿名方法的表现形式能否更加极致的简洁呢?当然,如果不考虑可读性的话,我们还可以将匿名方法写成这样的形式:

vList.ForEach(delegate(Vector3 obj) {Debug.Log(obj.normalized.ToString());});

当然,这里仅仅是给各位读者们一个参考,事实上这种可读性很差的形式是不被推荐的。

除了Action<T>这种返回类型为void的委托类型之外,上文还提到了另一种委托类型,即Func<T>。所以上面的代码我们可以修改为如下的形式,使得匿名方法可以有返回值。

using UnityEngine;

using System;

using System.Collections;

using System.Collections.Generic;



public class DelegateTest : MonoBehaviour {



       // Use this for initialization

       void Start () {

              Func<string, string> tellMeYourName = delegate(string name) {

                     string intro = "My name is ";

                     return intro + name;

              };



              Func<int, int, int> tellMeYourAge = delegate(int currentYear, int birthYear) {

                     return currentYear - birthYear;

              };



              Debug.Log(tellMeYourName("chenjiadong"));

              Debug.Log(tellMeYourAge(2015, 1989));

       }



       // Update is called once per frame

       void Update () {



       }

}

在匿名方法中,我们使用了return来返回指定类型的值,并且将匿名方法赋值给了Func<T>委托类型的实例。将上面这个C#脚本运行,在Unity3D的调试窗口我们可以看到输出了如下内容:

My name is chenjiadong

UnityEngine.Debug:Log(Object)

26

UnityEngine.Debug:Log(Object)

可以看到,我们通过tellMeYourName和tellMeYourAge这两个委托实例分别调用了我们定义的匿名方法。

当然,在C#语言中,除了刚刚提到过的Action<T>和Func<T>之外,还有一些我们在实际的开发中可能会遇到的预置的委托类型,例如返回值为bool型的委托类型Predicate<T>。它的签名如下:

public delegate bool Predicate<T> (T Obj);

而Predicate<T>委托类型常常会在过滤和匹配目标时发挥作用。下面让我们来再来看一个小例子。

9159.com 99159.com 10

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<BaseUnit> bList = new List<BaseUnit>();

              bList.Add(new Soldier());

              bList.Add(new Hero());

              bList.Add(new Soldier());

              bList.Add(new Soldier());

              bList.Add(new Soldier());

              bList.Add(new Soldier());

              bList.Add(new Hero());

              Predicate<BaseUnit> isHero = delegate(BaseUnit obj) {

                     return obj.IsHero;

              };



              foreach(BaseUnit unit in bList)

              {

                     if(isHero(unit))

                            CountHeroNum();

                     else

                            CountSoldierNum();

              }

              Debug.Log("英雄的个数为:" + this.heroCount);

              Debug.Log("士兵的个数为:" + this.soldierCount);

       }



       private void CountHeroNum()

       {

              this.heroCount++;

       }



       private void CountSoldierNum()

       {

              this.soldierCount++;

       }



       // Update is called once per frame

       void Update () {



       }

}

View Code

上面这段代码通过使用Predicate委托类型判断基础单位(BaseUnit)到底是士兵(Soldier)还是英雄(Hero),进而统计列表中士兵和英雄的数量。正如我们刚刚所说的Predicate主要用来做匹配和过滤,那么上述代码运行之后,输出如下的内容:

英雄的个数为:2

UnityEngine.Debug:Log(Object)

士兵的个数为:5

UnityEngine.Debug:Log(Object)

当然除了过滤和匹配目标,我们常常还会碰到对列表按照某一种条件进行排序的情况。例如要对按照英雄的最大血量进行排序或者按照英雄的战斗力来进行排序等等,可以说是按照要求排序是游戏系统开发过程中最常见的需求之一。那么是否也可以通过委托和匿名方法来方便的实现排序功能呢?C#又是否为我们预置了一些便利的“工具”呢?答案仍然是肯定的。我们可以方便的通过C#提供的Comparison<T>委托类型结合匿名方法来方便的为列表进行排序。

Comparison<T>的签名如下:

public delegate int Comparison(in T)(T x, T y)

由于Comparison<T>委托类型是IComparison<T>接口的委托版本,因而我们可以进一步来分析一下它的两个参数以及返回值。如下表:

参数

类型

作用

x

T

要比较的第一个对象

y

T

要比较的第二个对象

返回值

含义

小于0

x小于y。

等于0

x等于y。

大于0

x大于y。

 

 

 

 

好了,现在我们已经明确了Comparison<T>委托类型的参数和返回值的意义。那么下面我们就通过定义匿名方法来使用它对英雄(Hero)列表按指定的标准进行排序吧。

首先我们重新定义Hero类,提供英雄的属性数据。

9159.com 119159.com 12

using UnityEngine;

using System.Collections;



public class Hero : BaseUnit{

       public int id;

       public float currentHp;

       public float maxHp;

       public float attack;

       public float defence;



       public Hero()

       {

       }



       public Hero(int id, float maxHp, float attack, float defence)

       {

              this.id = id;

              this.maxHp = maxHp;

              this.currentHp = this.maxHp;

              this.attack = attack;

              this.defence = defence;

       }



       public float PowerRank

       {

              get

              {

                     return 0.5f * maxHp + 0.2f * attack + 0.3f * defence;

              }

       }



       public override bool IsHero

       {

              get

              {

                     return true;

              }

       }
}

View Code

之后使用Comparison<T>委托类型和匿名方法来对英雄列表进行排序。

9159.com 139159.com 14

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> bList = new List<Hero>();

              bList.Add(new Hero(1, 1000f, 50f, 100f));

              bList.Add(new Hero(2, 1200f, 20f, 123f));

              bList.Add(new Hero(5, 800f, 100f, 125f));

              bList.Add(new Hero(3, 600f, 54f, 120f));

              bList.Add(new Hero(4, 2000f, 5f, 110f));

              bList.Add(new Hero(6, 3000f, 65f, 105f));



              //按英雄的ID排序

              this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){

                     return Obj.id.CompareTo(Obj2.id);

              },"按英雄的ID排序");

              //按英雄的maxHp排序

              this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){

                     return Obj.maxHp.CompareTo(Obj2.maxHp);

              },"按英雄的maxHp排序");

              //按英雄的attack排序

              this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){

                     return Obj.attack.CompareTo(Obj2.attack);

              },"按英雄的attack排序");

              //按英雄的defense排序

              this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){

                     return Obj.defence.CompareTo(Obj2.defence);

              },"按英雄的defense排序");

              //按英雄的powerRank排序

              this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){

                     return Obj.PowerRank.CompareTo(Obj2.PowerRank);

              },"按英雄的powerRank排序");



       }



       public void SortHeros(List<Hero> targets ,Comparison<Hero> sortOrder, string orderTitle)

       {

//           targets.Sort(sortOrder);

              Hero[] bUnits = targets.ToArray();

              Array.Sort(bUnits, sortOrder);

              Debug.Log(orderTitle);

              foreach(Hero unit in bUnits)

              {

                     Debug.Log("id:" + unit.id);

                     Debug.Log("maxHp:" + unit.maxHp);

                     Debug.Log("attack:" + unit.attack);

                     Debug.Log("defense:" + unit.defence);

                     Debug.Log("powerRank:" + unit.PowerRank);

              }

       }





       // Update is called once per frame

       void Update () {



       }

}

View Code

这样,我们可以很方便的通过匿名函数来实现按英雄的ID排序、按英雄的maxHp排序、按英雄的attack排序、按英雄的defense排序以及按英雄的powerRank排序的要求,而无需为每一种排序都单独写一个独立的方法。

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

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

9159.com 15

9159.com 16

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);

       }

}

9159.com 17

将这个脚本挂载在游戏物体上,运行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的英雄的逻辑。下面我们就来试一试吧。

9159.com 18

9159.com 19

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 () {



       }

}

9159.com 20

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

回到目录

下面这段代码说明编译器在这种情形下采用的模式:

0x01 不必构造委托对象

委托的一种常见的使用方式,就像下面的这行代码一样:

this.unit.OnSubHp += new BaseUnit.SubHpHandler(this.OnSubHp);

其中括号中的OnSubHp是方法,该方法的定义如下:

private void OnSubHp (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);

    }

上面列出的第一行代码的意思是向this.unit的OnSubHp事件登记方法OnSubHp的地址,当OnSubHp事件被触发时通知调用OnSubHp方法。而这行代码的意义在于,通过构造SubHpHandler委托类型的实例来获取一个将回调方法OnSubHp进行包装的包装器,以确保回调方法只能以类型安全的方式调用。同时通过这个包装器,我们还获得了对委托链的支持。但是,更多的程序员显然更倾向于简单的表达方式,他们无需真正了解创建委托实例以获得包装器的意义,而只需要为事件注册相应的回调方法即可。例如下面的这行代码:

this.unit.OnSubHp += this.OnSubHp;

之所以能够这样写,我在之前的博客中已经有过解释。虽然“+=”操作符期待的是一个SubHpHandler委托类型的对象,而this.OnSubHp方法应该被SubHpHandler委托类型对象包装起来。但是由于C#的编译器能够自行推断,因而可以将构造SubHpHandler委托实例的代码省略,使得代码对程序员来说可读性更强。不过,编译器在幕后却并没有什么变化,虽然开发者的语法得到了简化,但是编译器生成CIL代码仍旧会创建新的SubHpHandler委托类型实例。

简而言之,C#允许通过指定回调方法的名称而省略构造委托类型实例的代码。

0x06 局部变量的存储位置

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

9159.com 21

9159.com 22

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 () {



       }

}

9159.com 23

将这个脚本挂载在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这个委托实例,却看到输出的结果依次是11、111、111,是在同一个变量的基础上累加而得到的结果。但是局部变量不是应该和方法一样分配在栈上,一旦方法返回便会随着TestCreateActionInstance方法对应的栈帧一起被销毁吗?但是,当我们再次调用委托实例的结果却表示,事实并非如此。TestCreateActionInstance方法的局部变量count并没有被分配在栈上,相反,编译器事实上在幕后为我们创建了一个临时的类用来保存这个变量。如果我们查看编译后的CIL代码,可能会更加直观一些。下面便是这段C#代码对应的CIL代码。

9159.com 24

.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

9159.com 25

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

.field  assembly  int32 count

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

9159.com 26

.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

9159.com 27

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

 

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

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

9159.com 289159.com 29

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);

       }

}

View Code

将这个脚本挂载在游戏物体上,运行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的英雄的逻辑。下面我们就来试一试吧。

9159.com 309159.com 31

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 () {



       }

}

View Code

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

此代码的结果输出是多少?答案是20  40  60,前面两个值,大家应该很容易就能看出来,但第三个值为什么是60呢?先来看看程序的执行流程:Closures函数调用GetClosureFunc函数并进入其中。函数调用语句中带了一个参数30。这是由于GetClosureFunc返回的是一个函数,即执行时再次调用了这个函数,进入GetClosureFunc函数中,首先val的值为10,通过internalAdd方法传入一个值10,因此第一个输出值为20,往下走,val的值变成30,通过internalAdd方法传入值10,于是第二个输出值为40。从这里我们大致可以看出,局部函数和局部变量如何在同一个作用域中起作用,显然,对局部变量的改变会影响internalAdd的值,尽管变量的改变发生在internalAdd最初的创建之后。最后,GetClosureFunc返回了internalAdd方法,以参数30再次调用这个函数,于是,结果成为60。

0x04 匿名方法和闭包

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

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

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

9159.com 32

int n = 0;

Del d = delegate() {

Debug.Log(++n);

};

9159.com 33

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

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

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

9159.com 34

9159.com 35

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());

              }

       }

}

9159.com 36

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

  • 参数i是一个外部变量,因为在它的作用域内声明了一个匿名方法,并且由于在匿名方法中使用了它,因而它是一个被捕捉的外部变量。
  • 变量outerValue是一个外部变量,这是由于在它的作用域内声明了一个匿名方法,但是和i不同的一点是outerValue并没有被匿名方法使用,因而它是一个没有被捕捉的外部变量。
  • 变量capturedOuterValue同样是一个外部变量,这也是因为在它的作用域内同样声明了一个匿名方法,但是capturedOuterValue和i一样被匿名方法所使用,因而它是一个被捕捉的外部变量。
  • 变量str不是外部变量,同样也不是EnclosingFunction这个方法的局部变量,相反它是一个匿名方法内部的局部变量。
  • 变量notOuterValue同样不是外部变量,这是因为在它所在的作用域中,并没有声明匿名方法。

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

回到目录

总结

作者  陈嘉栋(慕容小匹夫)

C#函数式程序设计之闭包机制

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

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

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

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

其中this.OnSubHp方法是我们为了响应事件而单独定义的一个方法,如果不定义这个方法而改由匿名方法直接订阅事件是否可以呢?答案是肯定的。

9159.com 37

9159.com 38

       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);



              };

       }

9159.com 39

在这里我们直接使用了delegate关键字定义了一个匿名方法来作为事件的回调方法而无需再单独定义一个方法。但是由于在这里我们要实现掉血的信息显示功能,因而看上去我们需要所有传入的参数。那么在少数情况下,我们不需要使用事件所要求的参数时,是否可以通过匿名方法在不提供参数的情况下订阅那个事件呢?答案也是肯定的,也就是说在不需要使用参数的情况下,我们通过匿名方法可以省略参数。还是在触发OnSubHp事件时,我们只需要告诉开发者事件触发即可,所以我们可以将AddListener方法改为下面这样:

9159.com 40

private void AddListener()

{

       this.unit.OnSubHp += this.OnSubHp;

       this.unit.OnSubHp += delegate {

              Debug.Log("呼救呼救,我被攻击了!");

       };

}

9159.com 41

之后,让我们运行一下修改后的脚本。可以在Unity3D的调试窗口看到如下内容的输出:

英雄暴击10000

UnityEngine.Debug:Log(Object)

呼救呼救,我被攻击了!

UnityEngine.Debug:Log(Object)

回到目录

初看起来,这并不真正符合逻辑。val应该是一个局部变量,它生存在栈中,当GetClosureFunc函数返回时,它就不在了,不是么?确实如此,这正是闭包的目的,当编译器会明白无误地警告这种情况会引起程序的崩溃时阻止变量值超出其作用域之外。

回到目录

C#函数式程序设计之作用域

0x02 匿名方法初探

在上一篇博文中,我们可以看到通常在使用委托时,往往要声明相应的方法,例如参数和返回类型必须符合委托类型确定的方法原型。而且,我们在实际的游戏开发过程中,往往也需要委托的这种机制来处理十分简单的逻辑,但对应的,我们必须要创建一个新的方法和委托类型匹配,这样做看起来将会使得代码变得十分臃肿。因而,在C#2的版本中,引入了匿名方法这种机制。什么是匿名方法?下面让我们来看一个小例子。

9159.com 42

using UnityEngine;

using System.Collections;

using System.Collections.Generic;

using System;



public class DelegateTest : MonoBehaviour {



       // Use this for initialization

       void Start () {

              //将匿名方法用于Action<T>委托类型

              Action<string> tellMeYourName = delegate(string name) {

                     string intro = "My name is ";

                     Debug.Log(intro + name);

              };



              Action<int> tellMeYourAge = delegate(int age) {

                     string intro = "My age is ";

                     Debug.Log(intro + age.ToString());

              };



              tellMeYourName("chenjiadong");

              tellMeYourAge(26);



       }



       // Update is called once per frame

       void Update () {



       }

}

9159.com 43

将这个DelegateTest脚本挂载在某个游戏场景中的物体上,运行编辑器,可以看到在调试窗口输出了如下内容。

My name is chenjiadong

UnityEngine.Debug:Log(Object)

My age is 26

UnityEngine.Debug:Log(Object)

在解释这段代码之前,我需要先为各位读者介绍一下常见的两个泛型委托类型:Action<T>以及Func<T>。它们的表现形式主要如下:

9159.com 44

public delegate void Action();

public delegate void Action<T1>(T1 arg1);

public delegate void Action<T1, T2>(T1 arg1, T2 arg2);

public delegate void Action<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3);

public delegate void Action<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);

public delegate void Action<T1, T2, T3, T4, T5>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5);

9159.com 45

从Action<T>的定义形式上可以看到。Action<T>是没有返回值得。适用于任何没有返回值的方法。

9159.com 46

public delegate TResult Func<TResult>();

public delegate TResult Func<T1, TResult>(T1 arg1);

public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);

public delegate TResult Func<T1, T2, T3, TResult>(T1 arg1, T2 arg2, T3 arg3);

public delegate TResult Func<T1, T2, T3, T4, TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);

public delegate TResult Func<T1, T2, T3, T4, T5, TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5);

9159.com 47

Func<T>委托的定义是相对于Action<T>来说。Action<T>是没有返回值的方法委托,Func<T>是有返回值的委托。返回值的类型,由泛型中定义的类型进行约束。

好了,各位读者对C#的这两个常见的泛型委托类型有了初步的了解之后,就让我们来看一看上面那段使用了匿名方法的代码吧。首先我们可以看到匿名方法的语法:先使用delegate关键字之后如果有参数的话则是参数部分,最后便是一个代码块定义对委托实例的操作。而通过这段代码,我们也可以看出一般方法体中可以做到事情,匿名函数同样可以做。而匿名方法的实现,同样要感谢编译器在幕后为我们隐藏了很多复杂度,因为在CIL代码中,编译器为源代码中的每一个匿名方法都创建了一个对应的方法,并且采用了和创建委托实例时相同的操作,将创建的方法作为回调函数由委托实例包装。而正是由于是编译器为我们创建的和匿名方法对应的方法,因而这些的方法名都是编译器自动生成的,为了不和开发者自己声明的方法名冲突,因而编译器生成的方法名的可读性很差。

当然,如果乍一看上面的那段代码似乎仍然很臃肿,那么能否不赋值给某个委托类型的实例而直接使用呢?答案是肯定的,同样也是我们最常使用的匿名方法的一种方式,那便是将匿名方法作为另一个方法的参数使用,因为这样才能体现出匿名方法的价值——简化代码。下面就让我们来看一个小例子,还记得List<T>列表吗?它有一个获取Action<T>作为参数的方法——ForEach,该方法对列表中的每个元素执行Action<T>所定义的操作。下面的代码将演示这一点,我们使用匿名方法对列表中的元素(向量Vector3)执行获取normalized的操作。

9159.com 48

using UnityEngine;

using System.Collections;

using System.Collections.Generic;



public class ActionTest : MonoBehaviour {



       // Use this for initialization

       void Start () {

              List<Vector3> vList = new List<Vector3>();

              vList.Add(new Vector3(3f, 1f, 6f));

              vList.Add(new Vector3(4f, 1f, 6f));

              vList.Add(new Vector3(5f, 1f, 6f));

              vList.Add(new Vector3(6f, 1f, 6f));

              vList.Add(new Vector3(7f, 1f, 6f));



              vList.ForEach(delegate(Vector3 obj) {

                     Debug.Log(obj.normalized.ToString());

              });

       }



       // Update is called once per frame

       void Update () {



       }

}

9159.com 49

我们可以看到,一个参数为Vector3的匿名方法:

delegate(Vector3 obj) {

       Debug.Log(obj.normalized.ToString());

}

实际上作为参数传入到了List的ForEach方法中。这段代码执行之后,我们可以在Unity3D的调试窗口观察输出的结果。内容如下:

(0.4, 0.1, 0.9)

UnityEngine.Debug:Log(Object)

(0.5, 0.1, 0.8)

UnityEngine.Debug:Log(Object)

(0.6, 0.1, 0.8)

UnityEngine.Debug:Log(Object)

(0.7, 0.1, 0.7)

UnityEngine.Debug:Log(Object)

(0.8, 0.1, 0.6)

UnityEngine.Debug:Log(Object)

那么,匿名方法的表现形式能否更加极致的简洁呢?当然,如果不考虑可读性的话,我们还可以将匿名方法写成这样的形式:

vList.ForEach(delegate(Vector3 obj) {Debug.Log(obj.normalized.ToString());});

当然,这里仅仅是给各位读者们一个参考,事实上这种可读性很差的形式是不被推荐的。

除了Action<T>这种返回类型为void的委托类型之外,上文还提到了另一种委托类型,即Func<T>。所以上面的代码我们可以修改为如下的形式,使得匿名方法可以有返回值。

9159.com 50

using UnityEngine;

using System;

using System.Collections;

using System.Collections.Generic;



public class DelegateTest : MonoBehaviour {



       // Use this for initialization

       void Start () {

              Func<string, string> tellMeYourName = delegate(string name) {

                     string intro = "My name is ";

                     return intro + name;

              };



              Func<int, int, int> tellMeYourAge = delegate(int currentYear, int birthYear) {

                     return currentYear - birthYear;

              };



              Debug.Log(tellMeYourName("chenjiadong"));

              Debug.Log(tellMeYourAge(2015, 1989));

       }



       // Update is called once per frame

       void Update () {



       }

}

9159.com 51

在匿名方法中,我们使用了return来返回指定类型的值,并且将匿名方法赋值给了Func<T>委托类型的实例。将上面这个C#脚本运行,在Unity3D的调试窗口我们可以看到输出了如下内容:

My name is chenjiadong

UnityEngine.Debug:Log(Object)

26

UnityEngine.Debug:Log(Object)

可以看到,我们通过tellMeYourName和tellMeYourAge这两个委托实例分别调用了我们定义的匿名方法。

当然,在C#语言中,除了刚刚提到过的Action<T>和Func<T>之外,还有一些我们在实际的开发中可能会遇到的预置的委托类型,例如返回值为bool型的委托类型Predicate<T>。它的签名如下:

public delegate bool Predicate<T> (T Obj);

而Predicate<T>委托类型常常会在过滤和匹配目标时发挥作用。下面让我们来再来看一个小例子。

9159.com 52

9159.com 53

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<BaseUnit> bList = new List<BaseUnit>();

              bList.Add(new Soldier());

              bList.Add(new Hero());

              bList.Add(new Soldier());

              bList.Add(new Soldier());

              bList.Add(new Soldier());

              bList.Add(new Soldier());

              bList.Add(new Hero());

              Predicate<BaseUnit> isHero = delegate(BaseUnit obj) {

                     return obj.IsHero;

              };



              foreach(BaseUnit unit in bList)

              {

                     if(isHero(unit))

                            CountHeroNum();

                     else

                            CountSoldierNum();

              }

              Debug.Log("英雄的个数为:" + this.heroCount);

              Debug.Log("士兵的个数为:" + this.soldierCount);

       }



       private void CountHeroNum()

       {

              this.heroCount++;

       }



       private void CountSoldierNum()

       {

              this.soldierCount++;

       }



       // Update is called once per frame

       void Update () {



       }

}

9159.com 54

上面这段代码通过使用Predicate委托类型判断基础单位(BaseUnit)到底是士兵(Soldier)还是英雄(Hero),进而统计列表中士兵和英雄的数量。正如我们刚刚所说的Predicate主要用来做匹配和过滤,那么上述代码运行之后,输出如下的内容:

英雄的个数为:2

UnityEngine.Debug:Log(Object)

士兵的个数为:5

UnityEngine.Debug:Log(Object)

当然除了过滤和匹配目标,我们常常还会碰到对列表按照某一种条件进行排序的情况。例如要对按照英雄的最大血量进行排序或者按照英雄的战斗力来进行排序等等,可以说是按照要求排序是游戏系统开发过程中最常见的需求之一。那么是否也可以通过委托和匿名方法来方便的实现排序功能呢?C#又是否为我们预置了一些便利的“工具”呢?答案仍然是肯定的。我们可以方便的通过C#提供的Comparison<T>委托类型结合匿名方法来方便的为列表进行排序。

Comparison<T>的签名如下:

public delegate int Comparison(in T)(T x, T y)

由于Comparison<T>委托类型是IComparison<T>接口的委托版本,因而我们可以进一步来分析一下它的两个参数以及返回值。如下表:

参数

类型

作用

x

T

要比较的第一个对象

y

T

要比较的第二个对象

返回值

含义

小于0

x小于y。

等于0

x等于y。

大于0

x大于y。

 

 

 

 

好了,现在我们已经明确了Comparison<T>委托类型的参数和返回值的意义。那么下面我们就通过定义匿名方法来使用它对英雄(Hero)列表按指定的标准进行排序吧。

首先我们重新定义Hero类,提供英雄的属性数据。

9159.com 55

9159.com 56

using UnityEngine;

using System.Collections;



public class Hero : BaseUnit{

       public int id;

       public float currentHp;

       public float maxHp;

       public float attack;

       public float defence;



       public Hero()

       {

       }



       public Hero(int id, float maxHp, float attack, float defence)

       {

              this.id = id;

              this.maxHp = maxHp;

              this.currentHp = this.maxHp;

              this.attack = attack;

              this.defence = defence;

       }



       public float PowerRank

       {

              get

              {

                     return 0.5f * maxHp + 0.2f * attack + 0.3f * defence;

              }

       }



       public override bool IsHero

       {

              get

              {

                     return true;

              }

       }
}

9159.com 57

之后使用Comparison<T>委托类型和匿名方法来对英雄列表进行排序。

9159.com 58

9159.com 59

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> bList = new List<Hero>();

              bList.Add(new Hero(1, 1000f, 50f, 100f));

              bList.Add(new Hero(2, 1200f, 20f, 123f));

              bList.Add(new Hero(5, 800f, 100f, 125f));

              bList.Add(new Hero(3, 600f, 54f, 120f));

              bList.Add(new Hero(4, 2000f, 5f, 110f));

              bList.Add(new Hero(6, 3000f, 65f, 105f));



              //按英雄的ID排序

              this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){

                     return Obj.id.CompareTo(Obj2.id);

              },"按英雄的ID排序");

              //按英雄的maxHp排序

              this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){

                     return Obj.maxHp.CompareTo(Obj2.maxHp);

              },"按英雄的maxHp排序");

              //按英雄的attack排序

              this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){

                     return Obj.attack.CompareTo(Obj2.attack);

              },"按英雄的attack排序");

              //按英雄的defense排序

              this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){

                     return Obj.defence.CompareTo(Obj2.defence);

              },"按英雄的defense排序");

              //按英雄的powerRank排序

              this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){

                     return Obj.PowerRank.CompareTo(Obj2.PowerRank);

              },"按英雄的powerRank排序");



       }



       public void SortHeros(List<Hero> targets ,Comparison<Hero> sortOrder, string orderTitle)

       {

//           targets.Sort(sortOrder);

              Hero[] bUnits = targets.ToArray();

              Array.Sort(bUnits, sortOrder);

              Debug.Log(orderTitle);

              foreach(Hero unit in bUnits)

              {

                     Debug.Log("id:" + unit.id);

                     Debug.Log("maxHp:" + unit.maxHp);

                     Debug.Log("attack:" + unit.attack);

                     Debug.Log("defense:" + unit.defence);

                     Debug.Log("powerRank:" + unit.PowerRank);

              }

       }





       // Update is called once per frame

       void Update () {



       }

}

9159.com 60

这样,我们可以很方便的通过匿名函数来实现按英雄的ID排序、按英雄的maxHp排序、按英雄的attack排序、按英雄的defense排序以及按英雄的powerRank排序的要求,而无需为每一种排序都单独写一个独立的方法。

回到目录

从技术角度来看,数据保存的位置很重要,编译器创建一个匿名类,并在GetClosureFunc中创建这个类的实例——如果不需要闭包起作用,则那个匿名函数只会与GetClosureFunc生存在同一个类中,最后,局部变量val实际上不再是一个局部变量,而是匿名类中的一个字段。其结果是,internalAdd现在可以引用保存在匿名类实例中的函数。这个实例中也包含变量val的数据。只要保持internalAdd的引用,变量val的值就一直保存着。

遗憾的是,有时我们无法把变量的值限制于函数的范围内。如果在程序的初始化时定义了几个变量,在后面需要反复用到它们,怎么办?一个可能的办法是使用闭包。

为了理解闭包的本质,我们分析几个使用闭包的例子:

回到动态创建函数思想:现在可以凭空创建新的函数,而且它的功能因参数而异。例如,下面这个函数把一个静态值加到一个参数上:

在C#中,变量的作用域是严格确定的。其本质是所有代码生存在类的方法中、所有变量只生存于声明它们的模块中或者之后的代码中。变量的值是可变的,一个变量越是公开,带来的问题就越严重。一般的原则是,变量的值最好保持不变,或者在最小的作用域内保存其值。一个纯函数最好只使用在自己的模块中定义的变量值,不访问其作用域之外的任何变量。

        private static void DynamicAdd()
        {
            var add5 = GetAddX(5);
            var add10 = GetAddX(10);
            Console.WriteLine(add5(10));
            Console.WriteLine(add10(10));
        }

        private static Func<int,int> GetAddX(int staticVal)
        {
            return x => staticVal + x;
        }

本文由9159.com发布于编程,转载请注明出处:回到目录,因而本文的主要目标便是介绍一些委

关键词: