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

来源:互联网 发布:mac怎么关闭开机密码 编辑:程序博客网 时间:2024/06/04 18:33

建议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    .maxstack 3    .locals init (        [0] class [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Action> lists,        [1] class [mscorlib]System.Action t,        [2] class [mscorlib]System.Action CS$<>9__CachedAnonymousMethodDelegate1,        [3] class MyTest.Program/<>c__DisplayClass2 CS$<>8__locals3,        [4] bool CS$4$0000,        [5] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class [mscorlib]System.Action> CS$5$0001)    L_0000: nop     L_0001: newobj instance void [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Action>::.ctor()    L_0006: stloc.0     L_0007: ldnull     L_0008: stloc.2     L_0009: newobj instance void MyTest.Program/<>c__DisplayClass2::.ctor()    L_000e: stloc.3     L_000f: ldloc.3     L_0010: ldc.i4.0     L_0011: stfld int32 MyTest.Program/<>c__DisplayClass2::i    L_0016: br.s L_0044    L_0018: nop     L_0019: ldloc.2     L_001a: brtrue.s L_002b    L_001c: ldloc.3     L_001d: ldftn instance void MyTest.Program/<>c__DisplayClass2::<Main>b__0()    L_0023: newobj instance void [mscorlib]System.Action::.ctor(object, native int)    L_0028: stloc.2     L_0029: br.s L_002b    L_002b: ldloc.2     L_002c: stloc.1     L_002d: ldloc.0     L_002e: ldloc.1     L_002f: callvirt instance void [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Action>::Add(!0)    L_0034: nop     L_0035: nop     L_0036: ldloc.3     L_0037: dup     L_0038: ldfld int32 MyTest.Program/<>c__DisplayClass2::i    L_003d: ldc.i4.1     L_003e: add     L_003f: stfld int32 MyTest.Program/<>c__DisplayClass2::i    L_0044: ldloc.3     L_0045: ldfld int32 MyTest.Program/<>c__DisplayClass2::i    L_004a: ldc.i4.5     L_004b: clt     L_004d: stloc.s CS$4$0000    L_004f: ldloc.s CS$4$0000    L_0051: brtrue.s L_0018    L_0053: nop     L_0054: ldloc.0 

//以下省略
复制代码

L_0009行,发现编译器为我们创建了一个类“<>c__DisplayClass2”,并且在循环内部每次会为这个类的一个实例变量 i 赋值。

这个类的IL代码为:

复制代码
.class auto ansi sealed nested private beforefieldinit <>c__DisplayClass2    extends [mscorlib]System.Object{    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()    .method public hidebysig specialname rtspecialname instance void .ctor() cil managed    {    }    .method public hidebysig instance void <Main>b__0() cil managed    {    }    .field public int32 i}
复制代码

经过分析,会发现前面的这段代码实际和下面这段代码是一致的:

复制代码
        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());            }        }
复制代码

 

 

 

转自:《编写高质量代码改善C#程序的157个建议》陆敏技

0 0
原创粉丝点击