首先来看一个简单的例子。
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的一篇博客中(https://blogs.msdn.microsoft.com/ericlippert/2009/11/12/closing-over-the-loop-variable-considered-harmful/) 见到了这样一句话:
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.youkuaiyun.com/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); };
}
}
}
https://blogs.msdn.microsoft.com/ericlippert/2009/11/16/closing-over-the-loop-variable-part-two/
为了防止上面的文章失效,下面一篇博客进行对其翻译。