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)
{
//定义动作组
List<Action> actions = new List<Action>();
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)
{
//定义动作组
List<Action> actions = new List<Action>();
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]闭包一
//定义动作组
List<Action> actions = new List<Action>();
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();
//定义动作组
List<Action> actions = new List<Action>();
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()
{
//定义动作组
List<Action> actions = new List<Action>();
for (int i = 0; i < 10; i++)//[3]闭包二
{
actions.Add(() => Console.WriteLine(i));
}
//执行动作
foreach (Action action in actions)
action();
Console.ReadKey();
}
上面的程序相当于下面的示例代码:
static void Main()
{
//定义动作组
List<Action> actions = new List<Action>();
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
'<Main>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'::'<Main>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`1<class [mscorlib]System.Action> actions,
[2] int32 counter,
[3] class [mscorlib]System.Action V_3,
[4] bool V_4,
[5] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class [mscorlib]System.Action> 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<class [mscorlib]System.Action>::.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'::'<Main>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<class [mscorlib]System.Action>::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<!0> class [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Action>::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<class [mscorlib]System.Action>::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<class [mscorlib]System.Action>::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<class [mscorlib]System.Action>
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()
{
//定义动作组
List<Action> actions = new List<Action>();
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()
{
//定义动作组
List<Action> actions = new List<Action>();
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