考虑以下代码块:
int x = 1;
D foo = () =>
{
Console.WriteLine(x);
x = 2;
};
x = 3;
foo();
Console.WriteLine(x);
输出为:3,2。我试图了解这段代码运行时幕后发生的事情。
x 变量是如何变更的问题。 x inside<> _DiplayClass1中的x如何更改Program类中的x。它是在幕后做这样的事吗?
var temp = new <>c_DisplayClass1();
temp.x = this.x;
temp.<Main>b_0();
this.x = temp.x;
答案 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.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。