深入探讨实施封锁

时间:2015-07-23 11:34:32

标签: c# closures

考虑以下代码块:

int x = 1;
D foo = () =>
{
    Console.WriteLine(x);
    x = 2;
};

x = 3;
foo();
Console.WriteLine(x);

输出为:3,2。我试图了解这段代码运行时幕后发生的事情。

编译器生成这个新类: enter image description here

x 变量是如何变更的问题。 x inside<> _DiplayClass1中的x如何更改Program类中的x。它是在幕后做这样的事吗?

var temp = new <>c_DisplayClass1();
temp.x = this.x;
temp.<Main>b_0();
this.x = temp.x;

5 个答案:

答案 0 :(得分:3)

如果您查看Main中发生的情况,您会看到:

public static void Main(string[] args)
{
    Program.<>c__DisplayClass0_0 <>c__DisplayClass0_ = new Program.<>c__DisplayClass0_0();
    <>c__DisplayClass0_.x = 1;
    Action action = new Action(<>c__DisplayClass0_.<Main>b__0);
    <>c__DisplayClass0_.x = 3;
    action();
    Console.WriteLine(<>c__DisplayClass0_.x);
}

[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
    public int x;

    internal void <Main>b__0()
    {
        Console.WriteLine(this.x);
        this.x = 2;
    }
}

这使事情变得更加清晰。您会看到提升的x成员设置了两次,一次设置为1,然后设置为3。在b__0内,它再次设置为2。因此,您会看到实际更改发生在同一成员上。这就是关闭变量时会发生的情况。 实际变量被取消,而不是它的价值。

答案 1 :(得分:2)

因为x是一个局部变量,所以你的方法可以被翻译成与之相当(但不相等)的东西:

int x = 1;
var closure = new <>c_DisplayClass1();
closure.x = x;

closure.x = 3;                      // x = 3
closure.<Main>b_0();                // foo();
Console.WriteLine(closure.x);       // Console.WriteLine(x)

换句话说,变量x的使用将替换为closure.x

答案 2 :(得分:2)

查看完全解编的代码会有所帮助:

// Decompiled with JetBrains decompiler
// Type: Program
// Assembly: test, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: D26FF17C-3FD8-4920-BEFC-ED98BC41836A
// Assembly location: C:\temp\test.exe
// Compiler-generated code is shown

using System;
using System.Runtime.CompilerServices;

internal static class Program
{
  private static void Main()
  {
    Program.\u003C\u003Ec__DisplayClass1 cDisplayClass1 = new Program.\u003C\u003Ec__DisplayClass1();
    cDisplayClass1.x = 1;
    // ISSUE: method pointer
    Action action = new Action((object) cDisplayClass1, __methodptr(\u003CMain\u003Eb__0));
    cDisplayClass1.x = 3;
    action();
    Console.WriteLine(cDisplayClass1.x);
  }

  [CompilerGenerated]
  private sealed class \u003C\u003Ec__DisplayClass1
  {
    public int x;

    public \u003C\u003Ec__DisplayClass1()
    {
      base.\u002Ector();
    }

    public void \u003CMain\u003Eb__0()
    {
      Console.WriteLine(this.x);
      this.x = 2;
    }
  }
}

具体来说,看看Main如何重写:

  private static void Main()
  {
    Program.\u003C\u003Ec__DisplayClass1 cDisplayClass1 = new Program.\u003C\u003Ec__DisplayClass1();
    cDisplayClass1.x = 1;
    // ISSUE: method pointer
    Action action = new Action((object) cDisplayClass1, __methodptr(\u003CMain\u003Eb__0));
    cDisplayClass1.x = 3;
    action();
    Console.WriteLine(cDisplayClass1.x);
  }

您会看到受影响的x附加到从代码生成的闭包类中。以下行将x更改为3:

    cDisplayClass1.x = 3;

这与x背后的方法所引用的action相同。

答案 3 :(得分:1)

根据C#简而言之:

lambda表达式可以引用方法的局部变量和参数 在其中定义(外部变量)。

实施例

int factor = 2;
Func<int, int> multiplier = n => n * factor;
Console.WriteLine (multiplier (3));    // outputs 6

捕获的变量和关闭:

lambda表达式引用的外部变量称为捕获变量。一个 捕获变量的lambda表达式称为闭包

捕获变量在实际调用委托时进行评估,而不是在何时进行 变量被捕获

例如:

int factor = 2;
Func<int, int> multiplier = n => n * factor;
factor = 10;
Console.WriteLine (multiplier (3)); // output is 30

Lambda表达式本身可以更新捕获的变量:

int seed = 0;
Func<int> natural = () => seed++;
Console.WriteLine (natural()); // 0
Console.WriteLine (natural()); // 1

Console.WriteLine (seed); // 2

捕获的变量的生命周期延长到委托的生命周期。

以下 例如,局部变量种子通常会从范围中消失 自然完成执行。但是因为种子被捕获了,它的寿命就是 扩展到捕获委托的那个,自然:

static Func<int> Natural()
{
int seed = 0;
return () => seed++; // Returns a closure
}

static void Main()
{
Func<int> natural = Natural();
Console.WriteLine (natural()); // 0
Console.WriteLine (natural()); // 1
}

在lambda表达式中实例化的局部变量在每次调用时都是唯一的 委托实例。如果我们重构前面的例子来实例化种子 在lambda表达式中,我们得到一个不同的(在这种情况下,是不合需要的)结果:

static Func<int> Natural()
{
return() => { int seed = 0; return seed++; };
}
static void Main()
{
Func<int> natural = Natural();
Console.WriteLine (natural()); // 0
Console.WriteLine (natural()); // 0
}
  
    

捕获是通过“提升”捕获的内部实现的     变量到私有类的字段中。当方法是     在调用时,该类被实例化并且与委托一起生命周期     实例

  

捕获迭代变量

当您捕获for循环的迭代变量时,C#将该变量视为 虽然它是在循环之外声明的。这意味着捕获了相同的变量 在每次迭代中。以下程序写入333而不是写入012:

Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
actions [i] = () => **Console.Write (i)**; // closure here
foreach (Action a in actions) a(); // 333

每个闭包(以粗体显示)捕获相同的变量,i。 (这实际上是 当你认为我是一个变量,其值在循环迭代之间持续存在时; 如果你愿意,你甚至可以在循环体内明确地改变我。) 结果是,当稍后调用代理时,每个代表都会看到我的价值 在调用时 - 这是3。

以上示例与此相同:

Action[] actions = new Action[3];
int i = 0;
actions[0] = () => Console.Write (i);
i = 1;
actions[1] = () => Console.Write (i);
i = 2;
actions[2] = () => Console.Write (i);
i = 3;
foreach (Action a in actions) a(); // 333

在C#5中将更改分解为注意:

在C#5.0之前,foreach循环以相同的方式工作。

考虑这个例子:

Action[] actions = new Action[3];
int i = 0;
foreach (char c in "abc")
actions [i++] = () => Console.Write (c);

foreach (Action a in actions) a();

它会在 C#4.0 中输出 ccc ,但在 C#5.0 中会输出 abc

从书中引用:

  
    

这引起了相当大的混淆:与for循环不同,     foreach循环中的迭代变量是不可变的,因此是一个     我希望它被视为循环体的局部。好的     新闻是它已经在C#5.0和上面的例子中得到修复     现在写“abc。”

         

从技术上讲,这是一个重大改变,因为重新编译C#     C#5.0中的4.0程序可能会产生不同的结果。一般来说,     C#团队试图避免破坏变化;但是在这     一个案例,“休息”几乎肯定会表明未被发现     C#4.0程序中的错误而不是故意依赖     旧的行为。

  

答案 4 :(得分:-2)

正在发生的事情是int x就像 全球 变量一样,因此您可以在foo()内创建/更新其值像

这样的匿名方法
   `D foo = () =>
    {
        Console.WriteLine(x);
        x = 2;
    };` 

该方法尚未运行,它将在您调用foo()之后运行,因此输出为3,2。