深入探讨关闭的实施
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.
考虑以下代码块:
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.