本文共 8480 字,大约阅读时间需要 28 分钟。
using System;namespace Application{ class Test { Action action; public Test() { int value = 2046; action = () => Console.WriteLine(value); } static void Main(string[] args) { Test test = new Test(); test.action(); } }}
在 Test 构造函数里,局部变量 value 在构造函数执行结束后出栈,那么 C# 是如何实现在函数执行以后访问其中的局部变量的?
你必须了解:引用类型、值类型、引用、对象、值类型的值(简称值)。
关于引用、对象和值在内存的分配有如下几点规则: •对象分配在堆中。 •作为字段的引用分配在堆中(内嵌在对象中)。
•作为局部变量(参数也是局部变量)的引用分配在栈中。 •作为字段的值分配在堆中(内嵌在对象中)。 •作为局部变量(参数也是局部变量)的值用分配在栈中。 •局部变量只能存活于所在的作用域(方法中的大括号确定了作用域的长短)。注:按值传递和按引用传递也是需要掌握的知识点,C# 默认是按值传递的。
内层的函数可以引用包含在它外层的函数的变量,即使外层函数的执行已经终止。但该变量提供的值并非变量创建时的值,而是在父函数范围内的最终值。
闭包是将一些执行语句的封装,可以将封装的结果像对象一样传递,在传递时,这个封装依然能够访问到原上下文。
形成闭包有一些值得总结的非必要条件: 1、嵌套定义的函数。 2、匿名函数。 3、将函数作为参数或者返回值。 4、在.NET中,可以通过匿名委托形成闭包:函数可以作为参数传递,也可以作为返回值返回,或者作为函数变量。而在.NET中,这都可以通过委托来实现。这些是实现闭包的前提。
使用闭包,我们可以轻松的访问外层函数定义的变量,这在匿名方法中普遍使用。比如有如下场景,在winform应用程序中,我们希望做这么一个效果,当用户关闭窗体时,给用户一个提示框。
private void Form1_Load(object sender, EventArgs e){ string msg= "您将关闭当前对话框"; this.FormClosing += delegate { MessageBox.Show(msg); };}
匿名函数很容易的访问到了作用域之外的变量。
全局变量
public static int i;//这个不是闭包static void Main(string[] args){ //定义动作组 Listactions = new List (); for (int counter = 0; counter < 10; counter++) { i = counter; actions.Add(() => Console.WriteLine(i)); } i = 123; //执行动作 foreach (Action action in actions) action(); Console.ReadKey();}
public static int i;//这个不是闭包static void TempMethod(){ Console.WriteLine(i);}static void Main(string[] args){ //定义动作组 Listactions = new List (); for (int counter = 0; counter < 10; counter++) { i = counter; actions.Add(new Action(TempMethod)); } //执行动作 foreach (Action action in actions) action(); Console.ReadKey();}
闭包示例一
static void Main(){ int i;//[1]闭包一 //定义动作组 Listactions = new List (); for (int counter = 0; counter < 10; counter++) { i = counter; actions.Add(() => Console.WriteLine(i)); } //执行动作 foreach (Action action in actions) action(); Console.ReadKey();}
运行结果:
显然这个结果不是我们想要的,上面的程序相当于下面的示例代码:
static void Main(){ TempClass tc = new TempClass(); //定义动作组 Listactions = new List (); for (int counter = 0; counter < 10; counter++) { tc.i = counter; actions.Add(tc.TempMethod); } //执行动作 foreach (Action action in actions) action(); Console.ReadKey();}class TempClass{ public int i; public void TempMethod() { Console.WriteLine(i); }}
闭包示例二
static void Main(){ //定义动作组 Listactions = new List (); for (int i = 0; i < 10; i++)//[3]闭包二 { actions.Add(() => Console.WriteLine(i)); } //执行动作 foreach (Action action in actions) action(); Console.ReadKey();}
上面的程序相当于下面的示例代码:
static void Main(){ //定义动作组 Listactions = new List (); TempClass tc = new TempClass(); for (tc.i = 0; tc.i < 10; tc.i++) { actions.Add(new Action(tc.TempMethod)); } //执行动作 foreach (Action action in actions) action(); Console.ReadKey();}class TempClass{ public int i; public void TempMethod() { Console.WriteLine(i); }}
运行结果:
这个结果也不是我们预期的。以示例一为例说明代码运行机制:
首先:C#编译器 为我们生成了一个 ‘<>c__DisplayClass0_0’的类,一个 “< Main > b__0”的方法 和 一个 变量 i。这个public int32 i 的变量就是程序一开始我们定义的变量i,现在被包装到了类中。
.method assembly hidebysig instance void 'b__0'() cil managed{ // 代码大小 13 (0xd) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldfld int32 'CSharp闭包之局部变量一'.Program/'<>c__DisplayClass0_0'::i IL_0006: call void [mscorlib]System.Console::WriteLine(int32) IL_000b: nop IL_000c: ret} // end of method '<>c__DisplayClass0_0'::' b__0'
上面这个是”< Main > b__0”方法的IL代码:就是输出
System.Console::WriteLine(int32)
下面是Main主程序的IL代码:
.method private hidebysig static void Main() cil managed{ .entrypoint // 代码大小 140 (0x8c) .maxstack 4 .locals init ([0] class 'CSharp闭包之局部变量一'.Program/'<>c__DisplayClass0_0' 'CS$<>8__locals0', [1] class [mscorlib]System.Collections.Generic.List`1actions, [2] int32 counter, [3] class [mscorlib]System.Action V_3, [4] bool V_4, [5] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator V_5, [6] class [mscorlib]System.Action action) IL_0000: newobj instance void 'CSharp闭包之局部变量一'.Program/'<>c__DisplayClass0_0'::.ctor() IL_0005: stloc.0 IL_0006: nop IL_0007: newobj instance void class [mscorlib]System.Collections.Generic.List`1 ::.ctor() IL_000c: stloc.1 IL_000d: ldc.i4.0 IL_000e: stloc.2 IL_000f: br.s IL_0044 IL_0011: nop IL_0012: ldloc.0 IL_0013: ldloc.2 IL_0014: stfld int32 'CSharp闭包之局部变量一'.Program/'<>c__DisplayClass0_0'::i IL_0019: ldloc.1 IL_001a: ldloc.0 IL_001b: ldfld class [mscorlib]System.Action 'CSharp闭包之局部变量一'.Program/'<>c__DisplayClass0_0'::'<>9__0' IL_0020: dup IL_0021: brtrue.s IL_0039 IL_0023: pop IL_0024: ldloc.0 IL_0025: ldloc.0 IL_0026: ldftn instance void 'CSharp闭包之局部变量一'.Program/'<>c__DisplayClass0_0'::' b__0'() IL_002c: newobj instance void [mscorlib]System.Action::.ctor(object, native int) IL_0031: dup IL_0032: stloc.3 IL_0033: stfld class [mscorlib]System.Action 'CSharp闭包之局部变量一'.Program/'<>c__DisplayClass0_0'::'<>9__0' IL_0038: ldloc.3 IL_0039: callvirt instance void class [mscorlib]System.Collections.Generic.List`1 ::Add(!0) IL_003e: nop IL_003f: nop IL_0040: ldloc.2 IL_0041: ldc.i4.1 IL_0042: add IL_0043: stloc.2 IL_0044: ldloc.2 IL_0045: ldc.i4.s 10 IL_0047: clt IL_0049: stloc.s V_4 IL_004b: ldloc.s V_4 IL_004d: brtrue.s IL_0011 IL_004f: nop IL_0050: ldloc.1 IL_0051: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator class [mscorlib]System.Collections.Generic.List`1 ::GetEnumerator() IL_0056: stloc.s V_5 .try { IL_0058: br.s IL_006b IL_005a: ldloca.s V_5 IL_005c: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator ::get_Current() IL_0061: stloc.s action IL_0063: ldloc.s action IL_0065: callvirt instance void [mscorlib]System.Action::Invoke() IL_006a: nop IL_006b: ldloca.s V_5 IL_006d: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator ::MoveNext() IL_0072: brtrue.s IL_005a IL_0074: leave.s IL_0085 } // end .try finally { IL_0076: ldloca.s V_5 IL_0078: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator IL_007e: callvirt instance void [mscorlib]System.IDisposable::Dispose() IL_0083: nop IL_0084: endfinally } // end handler IL_0085: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey() IL_008a: pop IL_008b: ret} // end of method Program::Main
编译器生成IL代码后,将作用域外的变量i,放到了匿名类型‘<>c__DisplayClass0_0’中当做成员字段来使用,由此,本来应该在堆栈上的int型i,被编译器包装成了object类类型的成员字段,而object被存储在堆中。
其实C#并不会对每个需要捕获的值类型变量进行装箱操作,而是把所有捕获的变量统统放到同一个大“箱子”里——当编译器遇到需要变量捕获的情况时,它会默默地在后台构造一个匿名类型,这个匿名类型包含了每一个闭包所捕获的变量(包括值类型变量和引用类型变量)作为它的一个公有字段。这样,编译器就可以维护那些在匿名函数或lambda表达式中出现的外部变量了。
总结
编译器将闭包引用的局部变量转换为匿名类型的字段,导致了局部变量分配在堆中。
如何避免闭包陷阱呢?C#中普遍的做法是,将匿名函数引用的变量用一个临时变量保存下来,然后在匿名函数中使用临时变量。
闭包示例三
static void Main(){ //定义动作组 Listactions = new List (); for (int counter = 0; counter < 10; counter++) { int i;//[1]闭包三 i = counter; //int copy = counter;//换种写法 actions.Add(() => Console.WriteLine(i)); } //执行动作 foreach (Action action in actions) action(); Console.ReadKey();}
上面的程序相当于下面的示例代码:
static void Main(){ //定义动作组 Listactions = new List (); for (int counter = 0; counter < 10; counter++) { TempClass tc = new TempClass(); tc.i = counter; actions.Add(tc.TempMethod); } //执行动作 foreach (Action action in actions) action(); Console.ReadKey();}class TempClass{ public int i; public void TempMethod() { Console.WriteLine(i); }}
运行结果:
与此同时,我们也可以在知道闭包的副作用的情况下(内层的函数可以引用包含在它外层的函数的变量,即使外层函数的执行已经终止。但该变量提供的值并非变量创建时的值,而是在父函数范围内的最终值)加以利用。
转自:https://blog.csdn.net/cjolj/article/details/60868305