购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

建议38:小心闭包中的陷阱

在介绍闭包的陷阱之前,先来看一段代码,设想一下其输出是什么:


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

}

} +FZsgHKq0RPAmRgnE1LJOjtyTbLy83Kjysf47viy4XQYTuoEmawoaCAq7qInIzSn


点击中间区域
呼出菜单
上一章
目录
下一章
×

打开