C# 循环中捕获的变量

发布于 2024-07-08 04:46:14 字数 452 浏览 10 评论 0原文

我遇到了一个关于 C# 的有趣问题。 我有如下代码。

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    actions.Add(() => variable * 2);
    ++ variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

我期望它输出 0、2、4、6、8。然而,它实际上输出了 5 个 10。

看来这是由于所有操作都引用了一个捕获的变量。 因此,当它们被调用时,它们都有相同的输出。

有没有办法解决这个限制,让每个操作实例都有自己的捕获变量?

I met an interesting issue about C#. I have code like below.

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    actions.Add(() => variable * 2);
    ++ variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

I expect it to output 0, 2, 4, 6, 8. However, it actually outputs five 10s.

It seems that it is due to all actions referring to one captured variable. As a result, when they get invoked, they all have same output.

Is there a way to work round this limit to have each action instance have its own captured variable?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

深府石板幽径 2024-07-15 04:46:15

正如其他人所说,它与循环无关。这是C#中匿名函数体中变量捕获机制的作用。
当您定义 lambda 作为示例时;

actions.Add(() => variable * 2);

编译器为 lambda 函数生成一个容器类,如 <>c__DisplayClass0_0
() =>; ()=> 变量 * 2。

在生成的类(容器)内部,它生成一个名为 variable 的字段,其中包含一个具有相同名称的捕获变量和包含 lambda 主体的方法 b__0() 。

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

internal int <Main>b__0()
{
    return variable * 2;
}
}

而名为 variable 的局部变量则成为容器类的一个字段 (<>c__DisplayClass0_0),

<>c__DisplayClass0_.variable = 0;
while (<>c__DisplayClass0_.variable < 5)
{
    list.Add(new Func<int>(<>c__DisplayClass0_.<Main>b__0));
    <>c__DisplayClass0_.variable++;
}

因此递增变量会依次递增容器类的字段,并且因为我们获得了容器类的一个实例while 循环的所有迭代,我们得到相同的输出,即 10。

在此处输入图像描述

您可以通过将循环体内捕获的变量重新分配给新的局部变量来防止

while (variable < 5)
{
    var index = variable; // <= this line
    actions.Add(() => index * 2);
    ++ variable;
}

这种行为顺便说一句,这种行为仍然有效在 .Net 8 Preview 中,我发现这种行为非常错误且具有欺骗性。

As others said It has nothing to do with loops.It is the effect of the variable capture mechanism in the body of anonymous functions in C#.
When you define lambda as your example ;

actions.Add(() => variable * 2);

Compiler generates a container class like <>c__DisplayClass0_0 for the lambda function
() => () => variable * 2.

Inside of the generated class(container) it generates a field named as variable, having a captured variable with the same name and the method b__0() containing the body of the lambda.

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

internal int <Main>b__0()
{
    return variable * 2;
}
}

And than local variable named variable, becomes a field of a container class(<>c__DisplayClass0_0)

<>c__DisplayClass0_.variable = 0;
while (<>c__DisplayClass0_.variable < 5)
{
    list.Add(new Func<int>(<>c__DisplayClass0_.<Main>b__0));
    <>c__DisplayClass0_.variable++;
}

So incrementing variable results in turn incrementing field of container class and because we get one instance of the container class for all iterations of the while loop,we get same output that is 10.

enter image description here

You can prevent by reassigning captured variable inside the body of the loop to a new local variable

while (variable < 5)
{
    var index = variable; // <= this line
    actions.Add(() => index * 2);
    ++ variable;
}

By the way,this behaviour still is valid with .Net 8 Preview and I find this behaviour is very buggy and deceptive.

暖树树初阳… 2024-07-15 04:46:15
for (int n=0; n < 10; n++) //forloop syntax
foreach (string item in foo) foreach syntax
for (int n=0; n < 10; n++) //forloop syntax
foreach (string item in foo) foreach syntax
记忆之渊 2024-07-15 04:46:15

这称为闭包问题,
只需使用复制变量即可。

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    int i = variable;
    actions.Add(() => i * 2);
    ++ variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

It is called the closure problem,
simply use a copy variable, and it's done.

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    int i = variable;
    actions.Add(() => i * 2);
    ++ variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}
寂寞陪衬 2024-07-15 04:46:15

由于这里没有人直接引用 ECMA-334

10.4.4.10 对于语句

对以下形式的 for 语句进行明确赋值检查:

for (for-initializer; for-condition; for-iterator) embedded-statement

就像编写语句一样完成:

{
    for-initializer;
    while (for-condition) {
        embedded-statement;
    LLoop: for-iterator;
    }
}

进一步在规范中,

12.16.6.3 局部变量的实例化

当执行进入变量的作用域时,局部变量被视为被实例化。

[示例:例如,当调用以下方法时,局部变量 x 会被实例化并初始化三次 - 每次循环迭代一次。

static void F() {
  for (int i = 0; i < 3; i++) {
    int x = i * 2 + 1;
    ...
  }
}

但是,将 x 的声明移出循环会导致 x 的单个实例化:

static void F() {
  int x;
  for (int i = 0; i < 3; i++) {
    x = i * 2 + 1;
    ...
  }
}

示例结束]

如果不捕获,则无法准确观察局部变量实例化的频率 - 因为实例化的生命周期是不相交的,每个实例化可能只使用相同的存储位置。 然而,当匿名函数捕获局部变量时,实例化的效果就变得显而易见。

[示例:示例

using System;

delegate void D();

class Test{
  static D[] F() {
    D[] result = new D[3];
    for (int i = 0; i < 3; i++) {
      int x = i * 2 + 1;
      result[i] = () => { Console.WriteLine(x); };
    }
  return result;
  }
  static void Main() {
    foreach (D d in F()) d();
  }
}

产生输出:

1
3
5

但是,当x的声明移出循环时:

static D[] F() {
  D[] result = new D[3];
  int x;
  for (int i = 0; i < 3; i++) {
    x = i * 2 + 1;
    result[i] = () => { Console.WriteLine(x); };
  }
  return result;
}

输出是:

5
5
5

请注意,编译器被允许(但不要求)将三个实例化优化为单个委托实例(第 11.7.2 节)。

如果 for 循环声明迭代变量,则该变量本身被视为在循环外部声明。
[示例:因此,如果更改示例以捕获迭代变量本身:

static D[] F() {
  D[] result = new D[3];
  for (int i = 0; i < 3; i++) {
    result[i] = () => { Console.WriteLine(i); };
  }
  return result;
}

仅捕获迭代变量的一个实例,该实例会产生输出:

3
3
3

示例结束]

哦,是的,我想应该提到的是,在 C++ 中不会出现此问题,因为您可以选择是否通过值或引用捕获变量(请参阅: Lambda 捕获)。

Since no one here directly quoted ECMA-334:

10.4.4.10 For statements

Definite assignment checking for a for-statement of the form:

for (for-initializer; for-condition; for-iterator) embedded-statement

is done as if the statement were written:

{
    for-initializer;
    while (for-condition) {
        embedded-statement;
    LLoop: for-iterator;
    }
}

Further on in the spec,

12.16.6.3 Instantiation of local variables

A local variable is considered to be instantiated when execution enters the scope of the variable.

[Example: For example, when the following method is invoked, the local variable x is instantiated and initialized three times—once for each iteration of the loop.

static void F() {
  for (int i = 0; i < 3; i++) {
    int x = i * 2 + 1;
    ...
  }
}

However, moving the declaration of x outside the loop results in a single instantiation of x:

static void F() {
  int x;
  for (int i = 0; i < 3; i++) {
    x = i * 2 + 1;
    ...
  }
}

end example]

When not captured, there is no way to observe exactly how often a local variable is instantiated—because the lifetimes of the instantiations are disjoint, it is possible for each instantation to simply use the same storage location. However, when an anonymous function captures a local variable, the effects of instantiation become apparent.

[Example: The example

using System;

delegate void D();

class Test{
  static D[] F() {
    D[] result = new D[3];
    for (int i = 0; i < 3; i++) {
      int x = i * 2 + 1;
      result[i] = () => { Console.WriteLine(x); };
    }
  return result;
  }
  static void Main() {
    foreach (D d in F()) d();
  }
}

produces the output:

1
3
5

However, when the declaration of x is moved outside the loop:

static D[] F() {
  D[] result = new D[3];
  int x;
  for (int i = 0; i < 3; i++) {
    x = i * 2 + 1;
    result[i] = () => { Console.WriteLine(x); };
  }
  return result;
}

the output is:

5
5
5

Note that the compiler is permitted (but not required) to optimize the three instantiations into a single delegate instance (§11.7.2).

If a for-loop declares an iteration variable, that variable itself is considered to be declared outside of the loop.
[Example: Thus, if the example is changed to capture the iteration variable itself:

static D[] F() {
  D[] result = new D[3];
  for (int i = 0; i < 3; i++) {
    result[i] = () => { Console.WriteLine(i); };
  }
  return result;
}

only one instance of the iteration variable is captured, which produces the output:

3
3
3

end example]

Oh yea, I guess it should be mentioned that in C++ this problem doesn't occur because you can choose if the variable is captured by value or by reference (see: Lambda capture).

破晓 2024-07-15 04:46:14

是的 - 在循环内获取变量的副本:

while (variable < 5)
{
    int copy = variable;
    actions.Add(() => copy * 2);
    ++ variable;
}

您可以将其视为 C# 编译器每次命中变量声明时都会创建一个“新”局部变量。 事实上,它会创建适当的新闭包对象,如果您引用多个作用域中的变量,它会变得复杂(就实现而言),但它可以工作:)

请注意,此问题更常见的情况是使用 forforeach

for (int i=0; i < 10; i++) // Just one variable
foreach (string x in foo) // And again, despite how it reads out loud

有关此内容的更多详细信息,请参阅 C# 3.0 规范的第 7.14.4.2 节,以及我的 关于闭包的文章也有更多示例。

请注意,从 C# 5 编译器及更高版本开始(即使指定早期版本的 C#),foreach 的行为发生了变化,因此您不再需要进行本地复制。 请参阅此回答以获取更多详细信息。

Yes - take a copy of the variable inside the loop:

while (variable < 5)
{
    int copy = variable;
    actions.Add(() => copy * 2);
    ++ variable;
}

You can think of it as if the C# compiler creates a "new" local variable every time it hits the variable declaration. In fact it'll create appropriate new closure objects, and it gets complicated (in terms of implementation) if you refer to variables in multiple scopes, but it works :)

Note that a more common occurrence of this problem is using for or foreach:

for (int i=0; i < 10; i++) // Just one variable
foreach (string x in foo) // And again, despite how it reads out loud

See section 7.14.4.2 of the C# 3.0 spec for more details of this, and my article on closures has more examples too.

Note that as of the C# 5 compiler and beyond (even when specifying an earlier version of C#), the behavior of foreach changed so you no longer need to make local copy. See this answer for more details.

小女人ら 2024-07-15 04:46:14

我相信您正在经历的是所谓的闭包 http://en.wikipedia.org/wiki /Closure_(计算机科学)。 你的lambda有一个对作用域在函数本身之外的变量的引用。 你的amba在你调用它之前不会被解释,一旦它被解释,它将获得变量在执行时的值。

I believe what you are experiencing is something known as Closure http://en.wikipedia.org/wiki/Closure_(computer_science). Your lamba has a reference to a variable which is scoped outside the function itself. Your lamba is not interpreted until you invoke it and once it is it will get the value the variable has at execution time.

春花秋月 2024-07-15 04:46:14

在幕后,编译器正在生成一个类来表示方法调用的闭包。 它为循环的每次迭代使用闭包类的单个实例。 代码看起来像这样,这使得更容易理解错误发生的原因:

void Main()
{
    List<Func<int>> actions = new List<Func<int>>();

    int variable = 0;

    var closure = new CompilerGeneratedClosure();

    Func<int> anonymousMethodAction = null;

    while (closure.variable < 5)
    {
        if(anonymousMethodAction == null)
            anonymousMethodAction = new Func<int>(closure.YourAnonymousMethod);

        //we're re-adding the same function 
        actions.Add(anonymousMethodAction);

        ++closure.variable;
    }

    foreach (var act in actions)
    {
        Console.WriteLine(act.Invoke());
    }
}

class CompilerGeneratedClosure
{
    public int variable;

    public int YourAnonymousMethod()
    {
        return this.variable * 2;
    }
}

这实际上不是示例中编译的代码,但我检查了我自己的代码,这看起来非常像编译器实际生成的代码。

Behind the scenes, the compiler is generating a class that represents the closure for your method call. It uses that single instance of the closure class for each iteration of the loop. The code looks something like this, which makes it easier to see why the bug happens:

void Main()
{
    List<Func<int>> actions = new List<Func<int>>();

    int variable = 0;

    var closure = new CompilerGeneratedClosure();

    Func<int> anonymousMethodAction = null;

    while (closure.variable < 5)
    {
        if(anonymousMethodAction == null)
            anonymousMethodAction = new Func<int>(closure.YourAnonymousMethod);

        //we're re-adding the same function 
        actions.Add(anonymousMethodAction);

        ++closure.variable;
    }

    foreach (var act in actions)
    {
        Console.WriteLine(act.Invoke());
    }
}

class CompilerGeneratedClosure
{
    public int variable;

    public int YourAnonymousMethod()
    {
        return this.variable * 2;
    }
}

This isn't actually the compiled code from your sample, but I've examined my own code and this looks very much like what the compiler would actually generate.

难以启齿的温柔 2024-07-15 04:46:14

这与循环无关。

触发此行为是因为您使用 lambda 表达式 () => 变量 * 2 其中外部作用域变量 实际上并未在 lambda 的内部作用域中定义。

Lambda 表达式(在 C#3+ 中,以及 C#2 中的匿名方法)仍然创建实际方法。 将变量传递给这些方法会遇到一些困境(按值传递?按引用传递?C# 采用引用 - 但这会带来另一个问题,即引用可能比实际变量更长寿)。 C# 解决所有这些困境的方法是创建一个新的辅助类(“闭包”),其中的字段对应于 lambda 表达式中使用的局部变量,方法对应于实际的 lambda 方法。 代码中对 variable 的任何更改实际上都会转换为 ClosureClass.variable 中的更改,

因此您的 while 循环会不断更新 ClosureClass.variable 直到它达到 10,然后 for 循环执行操作,这些操作都在同一个 ClosureClass.variable 上运行。

为了获得预期的结果,您需要在循环变量和正在关闭的变量之间创建分隔。 您可以通过引入另一个变量来做到这一点,即:

List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
    var t = variable; // now t will be closured (i.e. replaced by a field in the new class)
    actions.Add(() => t * 2);
    ++variable; // changing variable won't affect the closured variable t
}
foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

您还可以将闭包移动到另一个方法来创建这种分离:

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    actions.Add(Mult(variable));
    ++variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

您可以将 Mult 实现为 lambda 表达式(隐式闭包)

static Func<int> Mult(int i)
{
    return () => i * 2;
}

或使用实际的辅助类:

public class Helper
{
    public int _i;
    public Helper(int i)
    {
        _i = i;
    }
    public int Method()
    {
        return _i * 2;
    }
}

static Func<int> Mult(int i)
{
    Helper help = new Helper(i);
    return help.Method;
}

在任何情况下,“闭包”不是与循环相关的概念,而是与使用局部作用域变量的匿名方法/lambda 表达式相关 - 尽管一些不谨慎的循环使用表明了闭包陷阱。

This has nothing to do with loops.

This behavior is triggered because you use a lambda expression () => variable * 2 where the outer scoped variable not actually defined in the lambda's inner scope.

Lambda expressions (in C#3+, as well as anonymous methods in C#2) still create actual methods. Passing variables to these methods involve some dilemmas (pass by value? pass by reference? C# goes with by reference - but this opens another problem where the reference can outlive the actual variable). What C# does to resolve all these dilemmas is to create a new helper class ("closure") with fields corresponding to the local variables used in the lambda expressions, and methods corresponding to the actual lambda methods. Any changes to variable in your code is actually translated to change in that ClosureClass.variable

So your while loop keeps updating the ClosureClass.variable until it reaches 10, then you for loops executes the actions, which all operate on the same ClosureClass.variable.

To get your expected result, you need to create a separation between the loop variable, and the variable that is being closured. You can do this by introducing another variable, i.e.:

List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
    var t = variable; // now t will be closured (i.e. replaced by a field in the new class)
    actions.Add(() => t * 2);
    ++variable; // changing variable won't affect the closured variable t
}
foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

You could also move the closure to another method to create this separation:

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    actions.Add(Mult(variable));
    ++variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

You can implement Mult as a lambda expression (implicit closure)

static Func<int> Mult(int i)
{
    return () => i * 2;
}

or with an actual helper class:

public class Helper
{
    public int _i;
    public Helper(int i)
    {
        _i = i;
    }
    public int Method()
    {
        return _i * 2;
    }
}

static Func<int> Mult(int i)
{
    Helper help = new Helper(i);
    return help.Method;
}

In any case, "Closures" are NOT a concept related to loops, but rather to anonymous methods / lambda expressions use of local scoped variables - although some incautious use of loops demonstrate closures traps.

难理解 2024-07-15 04:46:14

解决这个问题的方法是将您需要的值存储在代理变量中,并捕获该变量。

IE

while( variable < 5 )
{
    int copy = variable;
    actions.Add( () => copy * 2 );
    ++variable;
}

The way around this is to store the value you need in a proxy variable, and have that variable get captured.

I.E.

while( variable < 5 )
{
    int copy = variable;
    actions.Add( () => copy * 2 );
    ++variable;
}
岁吢 2024-07-15 04:46:14

是的,您需要在循环内确定变量的作用域并以这种方式将其传递给 lambda:

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    int variable1 = variable;
    actions.Add(() => variable1 * 2);
    ++variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

Console.ReadLine();

Yes you need to scope variable within the loop and pass it to the lambda that way:

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    int variable1 = variable;
    actions.Add(() => variable1 * 2);
    ++variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

Console.ReadLine();
貪欢 2024-07-15 04:46:14

同样的情况也发生在多线程(C#,.NET 4.0]中。

请参阅以下内容code:

目的是按顺序打印 1,2,3,4,5

for (int counter = 1; counter <= 5; counter++)
{
    new Thread (() => Console.Write (counter)).Start();
}

输出很有趣(可能像 21334...)

唯一的解决方案是使用局部变量。

for (int counter = 1; counter <= 5; counter++)
{
    int localVar= counter;
    new Thread (() => Console.Write (localVar)).Start();
}

The same situation is happening in multi-threading (C#, .NET 4.0].

See the following code:

Purpose is to print 1,2,3,4,5 in order.

for (int counter = 1; counter <= 5; counter++)
{
    new Thread (() => Console.Write (counter)).Start();
}

The output is interesting! (It might be like 21334...)

The only solution is to use local variables.

for (int counter = 1; counter <= 5; counter++)
{
    int localVar= counter;
    new Thread (() => Console.Write (localVar)).Start();
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文