C# 循环中捕获的变量

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/271440/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-03 20:59:36  来源:igfitidea点击:

Captured variable in a loop in C#

c#closurescaptured-variable

提问by Morgan Cheng

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

我遇到了一个关于 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());
}

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

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

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?

有没有办法绕过这个限制,让每个动作实例都有自己的捕获变量?

采纳答案by Jon Skeet

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 :)

您可以将其视为 C# 编译器每次遇到变量声明时都会创建一个“新”局部变量。事实上,它会创建适当的新闭包对象,如果您在多个范围内引用变量,它会变得复杂(在实现方面),但它有效:)

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

请注意,此问题更常见的情况是使用foror 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 closureshas more examples too.

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

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

请注意,从 C# 5 编译器及更高版本开始(即使指定早期版本的 C#), 的行为已foreach更改,因此您不再需要进行本地复制。有关更多详细信息,请参阅此答案

回答by cfeduke

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

是的,您需要variable在循环内确定范围并以这种方式将其传递给 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();

回答by tjlevine

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

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

I.E.

IE

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

回答by TheCodeJunkie

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.

我相信您正在经历的是所谓的 Closure http://en.wikipedia.org/wiki/Closure_(computer_science)。您的 Lamba 引用了一个变量,该变量的范围在函数本身之外。你的 Lamba 在你调用它之前不会被解释,一旦它被解释,它就会在执行时获得变量的值。

回答by Sunil

The same situation is happening in multi-threading (C#, .NET4.0].

同样的情况发生在多线程(C# 、.NET4.0)中。

See the following code:

请参阅以下代码:

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

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

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

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

输出很有趣!(它可能像 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();
}

回答by gerrard00

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.

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

回答by David Refaeli

This has nothing to do with loops.

这与循环无关。

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

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

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 variablein your code is actually translated to change in that ClosureClass.variable

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

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

所以你的 while 循环不断更新ClosureClass.variable直到达到 10,然后你的 for 循环执行动作,这些动作都在同一个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)

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

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.

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

回答by Juned Khan Momin

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());
}

回答by Nathan Chappell

Since no one here directly quoted ECMA-334:

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

10.4.4.10 For statements

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

10.4.4.10 对于语句

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

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 xis instantiated and initialized three times—once for each iteration of the loop.

12.16.6.3 局部变量的实例化

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

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

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

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

但是,将 的声明移出x循环会导致 的单个实例化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 xis moved outside the loop:

但是,当 的声明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;
}

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:

请注意,允许(但不要求)编译器将三个实例优化为单个委托实例(第 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;
}

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).

哦,是的,我想应该提到的是,在 C++ 中不会发生这个问题,因为您可以选择是按值还是按引用捕获变量(请参阅:Lambda capture)。