C# 匿名函数引用局部变量解析

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

猜你喜欢

转载自blog.csdn.net/CNHK1225/article/details/80902449
今日推荐