在介绍闭包的陷阱之前,先来看一段代码,设想一下其输出是什么:
static void Main(string[]args)
{
List<Action>lists=new List<Action>();
for(int i=0;i<5;i++)
{
Action t=()=>
{
Console.WriteLine(i.ToString());
};
lists.Add(t);
}
foreach(Action t in lists)
{
t();
}
}
我们的设计意图是让匿名方法(在这里表现为Lambda表达式)接收参数i,并输出:
0
1
2
3
4
而实际的输出为:
5
5
5
5
5
这段代码并不像我们想象的那样简单,要完全理解运行时代码是怎么运行的,首先必须理解C#编译器为我们做了什么。查看本段代码的IL代码如下:
.method private hidebysig static void Main(string[]args)cil managed
{
.entrypoint
//代码大小123(0x7b)
.maxstack 3
.locals init([0]class[mscorlib]System.Collections.Generic.List1<class
[mscorlib]System.Action>lists,
[1]class[mscorlib]System.Action t,
[2]class[mscorlib]System.Action'CS$<>9__CachedAnonymousMethodDelegate1',
[3]class ConsoleApplication2.Program/'<>c__DisplayClass2'
'CS$<>8__locals3',
[4]class[mscorlib]System.Action V_4,
[5]valuetype[mscorlib]System.Collections.Generic.List1/
Enumerator<class[mscorlib]System.Action>CS$5$0000)
IL_0000:newobj instance void class[mscorlib]System.Collections
.Generic.List1<class[mscorlib]System.Action>::.ctor()
IL_0005:stloc.0
IL_0006:ldnull
IL_0007:stloc.2
IL_0008:newobj instance void
ConsoleApplication2.Program/'<>c__DisplayClass2'::.ctor()
IL_000d:stloc.3
IL_000e:ldloc.3
IL_000f:ldc.i4.0
IL_0010:stfld int32
ConsoleApplication2.Program/'<>c__DisplayClass2'::i
IL_0015:br.s IL_003e
IL_0017:ldloc.2
IL_0018:brtrue.s IL_0027
IL_001a:ldloc.3
IL_001b:ldftn instance void
ConsoleApplication2.Program/'<>c__DisplayClass2'::'<Main>b__0'()
IL_0021:newobj instance void
[mscorlib]System.Action::.ctor(object,native int)
IL_0026:stloc.2
IL_0027:ldloc.2
IL_0028:stloc.1
IL_0029:ldloc.0
IL_002a:ldloc.1
IL_002b:callvirt instance void class[mscorlib]System.Collections
.Generic.List1<class[mscorlib]System.Action>::Add(!0)
IL_0030:ldloc.3
IL_0031:dup
IL_0032:ldfld int32
ConsoleApplication2.Program/'<>c__DisplayClass2'::i
IL_0037:ldc.i4.1
IL_0038:add
IL_0039:stfld int32
ConsoleApplication2.Program/'<>c__DisplayClass2'::i
IL_003e:ldloc.3
IL_003f:ldfld int32
ConsoleApplication2.Program/'<>c__DisplayClass2'::i
IL_0044:ldc.i4.5
IL_0045:blt.s IL_0017
IL_0047:ldloc.0
//以下省略
在IL_0008行,发现编译器默默为我们创建了一个类"<>c__DisplayClass2",并且在循环内部每次会为这个类的一个实例变量i赋值。通过IL查看器,可以看到这个类,如图3-1所示。
图3-1 IL结构
经过分析,会发现前面的这段代码实际和下面这段代码是一致的:
static void Main(string[]args)
{
List<Action>lists=new List<Action>();
TempClass tempClass=new TempClass();
for(tempClass.i=0;tempClass.i<5;tempClass.i++)
{
Action t=tempClass.TempFuc;
lists.Add(t);
}
foreach(Action t in lists)
{
t();
}
}
class TempClass
{
public int i;
public void TempFuc()
{
Console.WriteLine(i.ToString());
}
}
这段代码所演示的就是闭包对象。所谓闭包对象,指的是上面这种情形中的TempClass对象(在第一段代码中,也就是编译器为我们生成的"<>c__DisplayClass2"对象)。如果匿名方法(Lambda表达式)引用了某个局部变量,编译器就会自动将该引用提升到该闭包对象中,即将for循环中的变量i修改成了引用闭包对象的公共变量i。这样一来,即使代码执行后离开了原局部变量i的作用域(如for循环),包含该闭包对象的作用域也还存在。理解了这一点,就能理解代码的输出了。
要实现本建议开始时所预期的输出,可以将闭包对象的产生放在for循环内部,也就是:
static void Main(string[]args)
{
List<Action>lists=new List<Action>();
for(int i=0;i<5;i++)
{
int temp=i;
Action t=()=>
{
Console.WriteLine(temp.ToString());
};
lists.Add(t);
}
foreach(Action t in lists)
{
t();
}
}
此代码和下面的代码是一致的:
static void Main(string[]args)
{
List<Action>lists=new List<Action>();
for(int i=0;i<5;i++)
{
TempClass tempClass=new TempClass();
tempClass.i=i;
Action t=tempClass.TempFuc;
lists.Add(t);
}
foreach(Action t in lists)
{
t();
}
}
class TempClass
{
public int i;
public void TempFuc()
{
Console.WriteLine(i.ToString());
}
}