C# 中的闭包

19 篇文章 3 订阅

首先来看一个简单的例子。

       var list = new Action[5];
            for (int i = 0; i < list.Length; i++)
            {
                list[i] = () => { Console.WriteLine(i); };
            }
            foreach (var item in list)
            {
                item();
            }

输出结果为:

5
5
5
5
5

通过这个简单的例子,我来简单讲解一下C#中的闭包。

概念:
In essence, a closure is a block of code which can be executed at a later time, but which maintains the environment in which it was first created - i.e. it can still use the local variables etc of the method which created it, even after that method has finished executing.

大概的意思是:从本质上说,闭包是一段可以在晚些时候执行的代码块,但是这段代码块依然维护着它第一个被创建时环境(执行上下文)。 即它仍可以使用创建它的方法中局部变量,即使那个方法已经执行完了。
当然在 C# 中通常通过匿名函数和 Lamada 表达式来实现闭包。
经过搜寻,我在 msdn 的一篇博客中 见到了这样一句话:

Because ()=>v means “return the current value of variable v“, not “return the value v was back when the delegate was created”. Closures close over variables, not over values
因为()=> v意味着“返回变量v的当前值”,而不是“返回值v在委托创建时返回”。 闭合变量,而不是值” 。

也就是说,在委托中填入的变量,是最终的那个变量。这样就合理解释了上面为何最终输出的结果都为5。因为i跳出循环时最终的值为5
接着我们先看一下通过IL,(关于IL指令说明,可以参考这篇文章的最后http://blog.csdn.net/u010533180/article/details/53064257) 反编译出来的代码,建议大家根据上一篇文章画流程图。

.method private hidebysig static void  ThreadThree() cil managed
{
  // 代码大小       130 (0x82)
  .maxstack  4
  .locals init ([0] class [mscorlib]System.Action[] list,
           [1] class [mscorlib]System.Action 'CS$<>9__CachedAnonymousMethodDelegateb',
           [2] class NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc' 'CS$<>8__localsd',
           [3] class [mscorlib]System.Action item,
           [4] bool CS$4$0000,
           [5] class [mscorlib]System.Action[] CS$6$0001,
           [6] int32 CS$7$0002)
  IL_0000:  nop
  //将整数值 5 作为 int32 推送到计算堆栈上。
  IL_0001:  ldc.i4.5
  //将对新的从零开始的一维数组(其元素属于特定类型)的对象引用推送到计算堆栈上。
  IL_0002:  newarr     [mscorlib]System.Action
  //从计算堆栈的顶部弹出当前值并将其存储到索引 0 处的局部变量列表中。
  IL_0007:  stloc.0
  //  将空引用(O 类型)推送到计算堆栈上。
  IL_0008:  ldnull
  //从计算堆栈的顶部弹出当前值并将其存储到索引 1 处的局部变量列表中。
  IL_0009:  stloc.1
  //创建一个值类型的新对象或新实例,并将对象引用(O 类型)推送到计算堆栈上。
  IL_000a:  newobj     instance void NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc'::.ctor()
  //从计算堆栈的顶部弹出当前值并将其存储到索引 2 处的局部变量列表中。
  IL_000f:  stloc.2
  //将索引 2 处的局部变量加载到计算堆栈上。
  IL_0010:  ldloc.2
  //将整数值 0 作为 int32 推送到计算堆栈上。
  IL_0011:  ldc.i4.0
  //用新值替换在对象引用或指针的字段中存储的值。
  IL_0012:  stfld      int32 NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc'::i
  //无条件地将控制转移到目标指令(短格式)。等于转移到了IL_0044指令
  IL_0017:  br.s       IL_0044
  IL_0019:  nop
 //将索引 0 处的局部变量加载到计算堆栈上。
  IL_001a:  ldloc.0
  //将索引 2 处的局部变量加载到计算堆栈上。
  IL_001b:  ldloc.2
  //查找对象中其引用当前位于计算堆栈的字段的值。等于查找i的值
  IL_001c:  ldfld      int32 NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc'::i
  //将索引 1 处的局部变量加载到计算堆栈上。
  IL_0021:  ldloc.1
  //如果 value 为 true、非空或非零,则将控制转移到目标指令(短格式)。 此时判断指令IL_0021的值如果为true ,则跳转到指令IL_0033
    IL_0022:  brtrue.s   IL_0033
 //将索引 2 处的局部变量加载到计算堆栈上。
  IL_0024:  ldloc.2
  //将指向实现特定方法的本机代码的非托管指针(native int 类型)推送到计算堆栈上。
  IL_0025:  ldftn      instance void NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc'::'<ThreadThree>b__a'()
  //创建一个值类型的新对象或新实例,并将对象引用(O 类型)推送到计算堆栈上。
  IL_002b:  newobj     instance void [mscorlib]System.Action::.ctor(object,
  //从计算堆栈的顶部弹出当前值并将其存储到索引 1 处的局部变量列表中。                                                                 native int)
  IL_0030:  stloc.1
  //无条件地将控制转移到目标指令(短格式)。等于转移到了IL_0044指令
  IL_0031:  br.s       IL_0033
 //将索引 1 处的局部变量加载到计算堆栈上。
  IL_0033:  ldloc.1
  //用计算堆栈上的对象 ref 值(O 类型)替换给定索引处的数组元素。这里其实指的就是那个Action类型
  IL_0034:  stelem.ref
  IL_0035:  nop
  //将索引 2 处的局部变量加载到计算堆栈上。
  IL_0036:  ldloc.2
  //复制计算堆栈上当前最顶端的值,然后将副本推送到计算堆栈上。
  IL_0037:  dup
  //查找对象中其引用当前位于计算堆栈的字段的值。等于查找i的值
  IL_0038:  ldfld      int32 NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc'::i
  将整数值 1 作为 int32 推送到计算堆栈上。
  IL_003d:  ldc.i4.1
  //将两个值相加并将结果推送到计算堆栈上。
  IL_003e:  add
  //用新值替换在对象引用或指针的字段中存储的值。
  IL_003f:  stfld      int32 NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc'::i
  //将索引 2 处的局部变量加载到计算堆栈上。
  IL_0044:  ldloc.2
  //查找对象中其引用当前位于计算堆栈的字段的值。等于查找i的值
  IL_0045:  ldfld      int32 NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc'::i
   //将索引 0 处的局部变量加载到计算堆栈上。
  IL_004a:  ldloc.0
  //  将从零开始的、一维数组的元素的数目推送到计算堆栈上。
  IL_004b:  ldlen
  //  将位于计算堆栈顶部的值转换为 int32。
  IL_004c:  conv.i4
  //  比较两个值。如果第一个值小于第二个值,则将整数值 1 (int32) 推送到计算堆栈上;反之,将 0 (int32) 推送到计算堆栈上。
  IL_004d:  clt
  //从计算堆栈的顶部弹出当前值并将其存储在局部变量列表中的 index 处(短格式)。
  IL_004f:  stloc.s    CS$4$0000  即 CS$4$0000 这个所在的索引
  //将特定索引处的局部变量加载到计算堆栈上(短格式)。
  IL_0051:  ldloc.s    CS$4$0000
  // 判断此时是否ture,如果为true 则跳转到指令IL_0019
  IL_0053:  brtrue.s   IL_0019
  IL_0055:  nop
  //将索引 0 处的局部变量加载到计算堆栈上。
  IL_0056:  ldloc.0
   //从计算堆栈的顶部弹出当前值并将其存储在局部变量列表中的 index 处(短格式)。
  IL_0057:  stloc.s    CS$6$0001
  //将整数值 0 作为 int32 推送到计算堆栈上。
  IL_0059:  ldc.i4.0
  //从计算堆栈的顶部弹出当前值并将其存储在局部变量列表中的 index 处(短格式)。
  IL_005a:  stloc.s    CS$7$0002
  //  无条件地将控制转移到目标指令(短格式)。 转移到IL_OO73
  IL_005c:  br.s       IL_0073
  //将特定索引处的局部变量加载到计算堆栈上(短格式)。
  IL_005e:  ldloc.s    CS$6$0001
    //将特定索引处的局部变量加载到计算堆栈上(短格式)。
  IL_0060:  ldloc.s    CS$7$0002
  //将位于指定数组索引处的包含对象引用的元素作为 O 类型(对象引用)加载到计算堆栈的顶部。
  IL_0062:  ldelem.ref
  //从计算堆栈的顶部弹出当前值并将其存储到索引 3 处的局部变量列表中。
  IL_0063:  stloc.3
  IL_0064:  nop
  //  将索引 3 处的局部变量加载到计算堆栈上。
  IL_0065:  ldloc.3
  //调用虚方法 执行Action 方法
  IL_0066:  callvirt   instance void [mscorlib]System.Action::Invoke()
  IL_006b:  nop
  IL_006c:  nop
  //将特定索引处的局部变量加载到计算堆栈上(短格式)。
  IL_006d:  ldloc.s    CS$7$0002
    //将整数值 1作为 int32 推送到计算堆栈上。
  IL_006f:  ldc.i4.1
  //将两个值相加并将结果推送到计算堆栈上。
  IL_0070:  add
 //从计算堆栈的顶部弹出当前值并将其存储在局部变量列表中的 index 处(短格式)。
  IL_0071:  stloc.s    CS$7$0002
  //将特定索引处的局部变量加载到计算堆栈上(短格式)。
  IL_0073:  ldloc.s    CS$7$0002
  //将特定索引处的局部变量加载到计算堆栈上(短格式)。
  IL_0075:  ldloc.s    CS$6$0001
  //  将从零开始的、一维数组的元素的数目推送到计算堆栈上。
  IL_0077:  ldlen
   //  将位于计算堆栈顶部的值转换为 int32。
  IL_0078:  conv.i4
  //比较两个值。如果第一个值小于第二个值,则将整数值 1 (int32) 推送到计算堆栈上;反之,将 0 (int32) 推送到计算堆栈上。
  IL_0079:  clt
    //将特定索引处的局部变量加载到计算堆栈上(短格式)。
  IL_007b:  stloc.s    CS$4$0000
    //将特定索引处的局部变量加载到计算堆栈上(短格式)。
  IL_007d:  ldloc.s    CS$4$0000
  //判断此时的值是否为true,如果为true 则跳转到指令IL_005e.这是应该判断数组是否遍历到了末尾
  IL_007f:  brtrue.s   IL_005e
  IL_0081:  ret
} // end of method ThreadDemo::ThreadThree

.NET Reflector 反编译的代码:

 Action[] actionArray = new Action[5];
    Action action = null;
    for (int i = 0; i < actionArray.Length; i++)
    {
        if (action == null)
        {
            action = () => Console.WriteLine(i);
        }
        actionArray[i] = action;
    }
    foreach (Action action2 in actionArray)
    {
        action2();
    }

那么上面的例子,如何输出0-4呢?根据上句话的提示,只需要创建一个变量,保存当前运行状态的值即可。修改后的结果为:

            var list = new Action[5];
            for (int i = 0; i < list.Length; i++)
            {
                int localI = i;
                list[i] = () => { Console.WriteLine(localI); };
            }
            foreach (var item in list)
            {
                item();
            }

或者是添加一个额外的方法也行,这样就相当于创建了一个局部变量。代码如下:

     var list = new Action[5];
            for (int i = 0; i < list.Length; i++)
            {
                AddList(list, i);
            }
            foreach (var item in list)
            {
                item();
            }
        static void AddList(Action[] list, int i)
        {
            list[i] = () => { Console.WriteLine(i); };
        }

上面两种方法运行输出的结果都为:0-4.

通过上面的分析,加深理解了C#中的闭包,以后要谨慎使用。匿名函数和 Lambda 表达式给我们的编程带来了许多快捷简单的实现,如(List.Max((a)=>a.Level)等写法)。但是我们要清醒的意识到这两个糖果后面还是有个”坑“(闭包)。这再次告诉我们技术工作人,要”知其然,也要知其所以然“。

下面给出完整的代码,其中有一些是我自己研究的,上面没有给出分析,建议读者自己分析,加深理解:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace NowCoderProgrammingProject
{
    class ThreadDemo
    {
        public static void Main()
        {
            ThreadOne();
            ThreadOne2();
            ThreadTwo();
            ThreadThree();
            ThreadThree1();
        }

        private static void ThreadOne()
        {
            for (int i = 0; i < 10; i++)
            {
                Thread t = new Thread(() =>
                {
                    Console.WriteLine(string.Format("{0}:{1}", Thread.CurrentThread.Name, i));
                });
                t.Name = string.Format("Thread{0}", i);
                t.IsBackground = true;
                t.Start();
            }
            Console.ReadLine();
        }

        private static void ThreadOne2()
        {
            for (int i = 0; i < 10; i++)
            {
                int localId = i;
                Thread t = new Thread(() =>
                {
                    Console.WriteLine(string.Format("{0}:{1}", Thread.CurrentThread.Name, localId));
                });
                t.Name = string.Format("Thread{0}", i);
                t.IsBackground = true;
                t.Start();
            }
            Console.ReadLine();
        }

        private static void ThreadTwo()
        {
            int id = 0;
            for (int i = 0; i < 10; i++)
            {
                NewMethod(i, id++);
            }
            Console.ReadLine();
        }

        private static void NewMethod(int i, int readTimeID)
        {
            Thread t = new Thread(() =>
            {
                Console.WriteLine(string.Format("{0}:{1}", Thread.CurrentThread.Name, readTimeID));
            });
            t.Name = string.Format("Thread{0}", i);
            t.IsBackground = true;
            t.Start();
        }

        static void ThreadThree()
        {
            var list = new Action[5];
            for (int i = 0; i < list.Length; i++)
            {
                list[i] = () => { Console.WriteLine(i); };
            }
            foreach (var item in list)
            {
                item();
            }
        }
        static void ThreadThree1()
        {
            var list = new Action[5];
            for (int i = 0; i < list.Length; i++)
            {
                int localI = i;
                list[i] = () => { Console.WriteLine(localI); };
            }
            foreach (var item in list)
            {
                item();
            }
        }
        static void ThreadThree2()
        {
            var list = new Action[5];
            for (int i = 0; i < list.Length; i++)
            {
                AddList(list, i);
            }
            foreach (var item in list)
            {
                item();
            }
        }
        static void AddList(Action[] list, int i)
        {
            list[i] = () => { Console.WriteLine(i); };
        }
    }
}

参考文章:

[1] Closing over the loop variable considered harmful
[2] Closing over the loop variable, part two

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值