深入探讨关闭的实施

Deep diving into the implementation of closures

考虑以下代码块:

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

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

输出为:3,2。我试图了解当此代码为 运行.

时幕后发生的情况

编译器生成这个新的 class:

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

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

发生的事情是 int x 就像 global 变量,所以你可以 reach/update 他的值在 foo() 中,当您创建像

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

该方法还不是 运行,它将在您调用 foo() 之后 运行 因此输出是 3,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

查看完全反编译的代码有帮助:

// 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 附加到从代码生成的闭包 class。以下行将 x 更改为 3:

    cDisplayClass1.x = 3;

这与 action 后面的方法所指的 x 相同。

如果您查看 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。因此,您看到实际更改发生在 同一成员 上。这就是关闭变量时发生的情况。 实际变量 被提升,而不是它的值。

根据 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

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

在下面 例如,局部变量 seed 通常会在以下情况下从作用域中消失 自然执行完。但是因为种子已经被捕获,它的寿命是 扩展到捕获委托,自然:

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
}

Capturing is internally implemented by “hoisting” the captured variables into fields of a private class. When the method is called, the class is instantiated and lifetime-bound to the delegate instance.

正在捕获迭代变量

当您捕获 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。 (这实际上使 当您认为 i 是一个变量,其值在循环迭代之间持续存在时,这是有意义的; 如果需要,您甚至可以在循环体内显式更改 i。) 结果是当稍后调用委托时,每个委托都会看到 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.

引自书本:

This caused considerable confusion: unlike with a for loop, the iteration variable in a foreach loop is immutable, and so one would expect it to be treated as local to the loop body. The good news is that it’s been fixed in C# 5.0, and the example above now writes “abc.”

Technically, this is a breaking change because recompiling a C# 4.0 program in C# 5.0 could create a different result. In general, the C# team tries to avoid breaking changes; however in this case, a “break” would almost certainly indicate an undetected bug in the C# 4.0 program rather than intentional reliance on the old behavior.